juju_scaleway.BaseCommand   A
last analyzed

Complexity

Total Complexity 8

Size/Duplication

Total Lines 36
Duplicated Lines 0 %
Metric Value
dl 0
loc 36
rs 10
wmc 8

3 Methods

Rating   Name   Duplication   Size   Complexity  
A __init__() 0 5 1
B check_preconditions() 0 21 6
A solve_constraints() 0 6 1
1
# -*- coding: utf-8 -*-
2
#
3
# Copyright (c) 2014-2015 Online SAS and Contributors. All Rights Reserved.
4
#                         Edouard Bonlieu <[email protected]>
5
#                         Julien Castets <[email protected]>
6
#                         Manfred Touron <[email protected]>
7
#                         Kevin Deldycke <[email protected]>
8
#
9
# Licensed under the BSD 2-Clause License (the "License"); you may not use this
10
# file except in compliance with the License. You may obtain a copy of the
11
# License at http://opensource.org/licenses/BSD-2-Clause
12
13
import logging
14
import time
15
import uuid
16
import yaml
17
18
from juju_scaleway import constraints
19
from juju_scaleway.exceptions import ConfigError, PrecheckError
20
from juju_scaleway import ops
21
from juju_scaleway.runner import Runner
22
23
24
logger = logging.getLogger("juju.scaleway")
25
26
27
class BaseCommand(object):
28
29
    def __init__(self, config, provider, environment):
30
        self.config = config
31
        self.provider = provider
32
        self.env = environment
33
        self.runner = Runner()
34
35
    def solve_constraints(self):
36
        start_time = time.time()
37
        image_map = constraints.get_images(self.provider.client)
38
        logger.debug("Looked up scaleway images in %0.2f seconds",
39
                     time.time() - start_time)
40
        return image_map[self.config.series]
41
42
    def check_preconditions(self):
43
        """Check for provider and configured environments.yaml.
44
        """
45
        env_name = self.config.get_env_name()
46
        with open(self.config.get_env_conf()) as handle:
47
            conf = yaml.safe_load(handle.read())
48
            if 'environments' not in conf:
49
                raise ConfigError(
50
                    "Invalid environments.yaml, no 'environments' section")
51
            if env_name not in conf['environments']:
52
                raise ConfigError(
53
                    "Environment %r not in environments.yaml" % env_name)
54
            env = conf['environments'][env_name]
55
            if not env['type'] in ('null', 'manual'):
56
                raise ConfigError(
57
                    "Environment %r provider type is %r must be 'null'" % (
58
                        env_name, env['type']))
59
            if env['bootstrap-host']:
60
                raise ConfigError(
61
                    "Environment %r already has a bootstrap-host" % (
62
                        env_name))
63
64
65
class Bootstrap(BaseCommand):
66
    """
67
    Actions:
68
    - Launch an server
69
    - Wait for it to reach running state
70
    - Update environment in environments.yaml with bootstrap-host address.
71
    - Bootstrap juju environment
72
    Preconditions:
73
    - named environment found in environments.yaml
74
    - environment provider type is null
75
    - bootstrap-host must be null
76
    - ? existing scaleway with matching env name does not exist.
77
    """
78
    def run(self):
79
        self.check_preconditions()
80
        image = self.solve_constraints()
81
        logger.info("Launching bootstrap host (eta 5m)...")
82
        params = dict(
83
            name="%s-0" % self.config.get_env_name(), image=image)
84
85
        machine = ops.MachineAdd(
86
            self.provider, self.env, params, series=self.config.series
87
        )
88
        server = machine.run()
89
90
        logger.info("Bootstrapping environment...")
91
        try:
92
            self.env.bootstrap_jenv(server.public_ip['address'])
93
        except:
94
            self.provider.terminate_server(server.id)
95
            raise
96
        logger.info("Bootstrap complete.")
97
98
    def check_preconditions(self):
99
        result = super(Bootstrap, self).check_preconditions()
100
        if self.env.is_running():
101
            raise PrecheckError(
102
                "Environment %s is already bootstrapped" % (
103
                    self.config.get_env_name()))
104
        return result
105
106
107
class ListMachines(BaseCommand):
108
109
    def run(self):
110
        env_name = self.config.get_env_name()
111
        header = "{:<8} {:<18} {:<8} {:<12} {:<10}".format(
112
            "Id", "Name", "Status", "Created", "Address")
113
114
        allmachines = self.config.options.all
115
        for server in self.provider.get_servers():
116
            name = server.name
117
118
            if not allmachines and not name.startswith('%s-' % env_name):
119
                continue
120
121
            if header:
122
                print(header)
123
                header = None
124
125
            if len(name) > 18:
126
                name = name[:15] + "..."
127
128
            print("{:<8} {:<18} {:<8} {:<12} {:<10}".format(
129
                server.id,
130
                name,
131
                server.state,
132
                server.creation_date[:-10],
133
                server.public_ip['address'] if server.public_ip else 'none'
134
            ).strip())
135
136
137
class AddMachine(BaseCommand):
138
139
    def run(self):
140
        self.check_preconditions()
141
        image = self.solve_constraints()
142
        logger.info("Launching %d servers...", self.config.num_machines)
143
144
        template = dict(
145
            image=image)
146
147
        for _ in range(self.config.num_machines):
148
            params = dict(template)
149
            params['name'] = "%s-%s" % (
150
                self.config.get_env_name(), uuid.uuid4().hex)
151
            self.runner.queue_op(
152
                ops.MachineRegister(
153
                    self.provider, self.env, params, series=self.config.series
154
                )
155
            )
156
157
        for (server, _) in self.runner.iter_results():
158
            logger.info(
159
                "Registered id:%s name:%s ip:%s as juju machine",
160
                server.id, server.name,
161
                server.public_ip['address'] if server.public_ip else None
162
            )
163
164
165
class TerminateMachine(BaseCommand):
166
167
    def run(self):
168
        """Terminate machine in environment.
169
        """
170
        self.check_preconditions()
171
        self._terminate_machines(lambda x: x in self.config.options.machines)
172
173
    def _terminate_machines(self, machine_filter):
174
        logger.debug("Checking for machines to terminate")
175
        status = self.env.status()
176
        machines = status.get('machines', {})
177
178
        # Using the api server-id can be the provider id, but
179
        # else it defaults to ip, and we have to disambiguate.
180
        remove = []
181
        for machine in machines:
182
            if machine_filter(machine):
183
                remove.append({
184
                    'address': machines[machine]['dns-name'],
185
                    'server_id': machines[machine]['instance-id'],
186
                    'machine_id': machine
187
                })
188
189
        address_map = dict([
190
            (d.public_ip['address'] if d.public_ip else None, d)
191
            for d in self.provider.get_servers()
192
        ])
193
        if not remove:
194
            return status, address_map
195
196
        logger.info(
197
            "Terminating machines %s",
198
            " ".join([machine['machine_id'] for machine in remove])
199
        )
200
201
        for machine in remove:
202
            server = address_map.get(machine['address'])
203
            env_only = False  # Remove from only env or also provider.
204
            if server is None:
205
                logger.warning(
206
                    "Couldn't resolve machine %s's address %s to server",
207
                    machine['machine_id'], machine['address']
208
                )
209
                # We have a machine in juju state that we couldn't
210
                # find in provider. Remove it from state so destroy
211
                # can proceed.
212
                env_only = True
213
                server_id = None
214
            else:
215
                server_id = server.id
216
            self.runner.queue_op(
217
                ops.MachineDestroy(
218
                    self.provider, self.env, {
219
                        'machine_id': machine['machine_id'],
220
                        'server_id': server_id
221
                    },
222
                    env_only=env_only
223
                )
224
            )
225
        for _ in self.runner.iter_results():
226
            pass
227
228
        return status, address_map
229
230
231
class DestroyEnvironment(TerminateMachine):
232
233
    def run(self):
234
        """Destroy environment.
235
        """
236
        self.check_preconditions()
237
        force = self.config.options.force
238
239
        # Manual provider needs machines removed prior to env destroy.
240
        def state_service_filter(machine):
241
            if machine == "0":
242
                return False
243
            return True
244
245
        if force:
246
            return self.force_environment_destroy()
247
248
        env_status, server_map = self._terminate_machines(
249
            state_service_filter
250
        )
251
252
        # sadness, machines are marked dead, but juju is async to
253
        # reality. either sleep (racy) or retry loop, 10s seems to
254
        # plenty of time.
255
        time.sleep(10)
256
257
        logger.info("Destroying environment")
258
        self.env.destroy_environment()
259
260
        # Remove the state server.
261
        bootstrap_host = env_status.get(
262
            'machines', {}).get('0', {}).get('dns-name')
263
        server = server_map.get(bootstrap_host)
264
        if server:
265
            logger.info("Terminating state server")
266
            self.provider.terminate_server(server.id)
267
        logger.info("Environment Destroyed")
268
269
    def force_environment_destroy(self):
270
        env_name = self.config.get_env_name()
271
        env_machines = [m for m in self.provider.get_servers()
272
                        if m.name.startswith("%s-" % env_name)]
273
274
        logger.info("Destroying environment")
275
        for machine in env_machines:
276
            self.runner.queue_op(
277
                ops.MachineDestroy(
278
                    self.provider, self.env, {'server_id': machine.id},
279
                    iaas_only=True
280
                )
281
            )
282
283
        for _ in self.runner.iter_results():
284
            pass
285
286
        # Fast destroy the client cache by removing the jenv file.
287
        self.env.destroy_environment_jenv()
288
        logger.info("Environment Destroyed")
289