Merge lp:~tvansteenburgh/juju-deployer/beta11 into lp:juju-deployer
- beta11
- Merge into trunk
Proposed by
Tim Van Steenburgh
Status: | Merged |
---|---|
Merged at revision: | 189 |
Proposed branch: | lp:~tvansteenburgh/juju-deployer/beta11 |
Merge into: | lp:juju-deployer |
Diff against target: |
1093 lines (+289/-128) 18 files modified
deployer/action/diff.py (+2/-2) deployer/action/importer.py (+6/-6) deployer/charm.py (+2/-1) deployer/cli.py (+20/-18) deployer/config.py (+8/-2) deployer/env/base.py (+33/-15) deployer/env/go.py (+44/-14) deployer/env/gui.py (+2/-2) deployer/env/mem.py (+4/-2) deployer/env/watchers.py (+32/-7) deployer/errors.py (+1/-1) deployer/service.py (+50/-27) deployer/tests/test_deployment.py (+41/-24) deployer/tests/test_goenv.py (+1/-1) deployer/tests/test_guienv.py (+1/-1) deployer/tests/test_guiserver.py (+4/-4) deployer/utils.py (+37/-0) tox.ini (+1/-1) |
To merge this branch: | bzr merge lp:~tvansteenburgh/juju-deployer/beta11 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
juju-deployers | Pending | ||
Review via email:
|
Commit message
Description of the change
Changes to support juju2 api changes.
To post a comment you must log in.
- 188. By Tim Van Steenburgh
-
s/lxc/lxd/
- 189. By Tim Van Steenburgh
-
Fix lxc/lxd placement support
- On juju1, lxd is not a valid placment directive (only lxc/kvm).
- On juju2, lxc/lxd/kvm are all supported, but lxc directives will
be changed to lxd before being passed to juju.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'deployer/action/diff.py' |
2 | --- deployer/action/diff.py 2016-05-12 20:22:06 +0000 |
3 | +++ deployer/action/diff.py 2016-07-29 13:47:45 +0000 |
4 | @@ -32,7 +32,7 @@ |
5 | self.env_state['services'][svc_name][ |
6 | 'constraints'] = self.env.get_constraints(svc_name) |
7 | self.env_state['services'][svc_name]['unit_count'] = len( |
8 | - self.env_status['services'][svc_name].get('units', {})) |
9 | + (self.env_status['services'][svc_name].get('units') or {})) |
10 | rels.update(self._load_rels(svc_name)) |
11 | self.env_state['relations'] = sorted(rels) |
12 | |
13 | @@ -61,7 +61,7 @@ |
14 | for r, eps in svc_rels.items(): |
15 | if src in eps: |
16 | if found: |
17 | - raise ValueError("Ambigious relations for service") |
18 | + raise ValueError("Ambigious relations for application") |
19 | found = r |
20 | return found |
21 | |
22 | |
23 | === modified file 'deployer/action/importer.py' |
24 | --- deployer/action/importer.py 2016-05-07 23:02:56 +0000 |
25 | +++ deployer/action/importer.py 2016-07-29 13:47:45 +0000 |
26 | @@ -32,7 +32,7 @@ |
27 | while delay > time.time(): |
28 | if svc.name in env_status['services']: |
29 | cur_units = len( |
30 | - env_status['services'][svc.name].get('units', ()) |
31 | + (env_status['services'][svc.name].get('units') or ()) |
32 | ) |
33 | if cur_units > 0: |
34 | break |
35 | @@ -43,7 +43,7 @@ |
36 | |
37 | if delta <= 0: |
38 | self.log.debug( |
39 | - " Service %r does not need any more units added.", |
40 | + " Application %r does not need any more units added.", |
41 | svc.name) |
42 | continue |
43 | |
44 | @@ -161,19 +161,19 @@ |
45 | in a machine spec, and units will be placed accordingly if this |
46 | flag is false. |
47 | """ |
48 | - self.log.info("Deploying services...") |
49 | + self.log.info("Deploying applications...") |
50 | env_status = self.env.status() |
51 | reloaded = False |
52 | |
53 | for svc in self.deployment.get_services(): |
54 | if svc.name in env_status['services']: |
55 | self.log.debug( |
56 | - " Service %r already deployed. Skipping" % svc.name) |
57 | + " Application %r already deployed. Skipping" % svc.name) |
58 | continue |
59 | |
60 | charm = self.deployment.get_charm_for(svc.name) |
61 | self.log.info( |
62 | - " Deploying service %s using %s", svc.name, |
63 | + " Deploying application %s using %s", svc.name, |
64 | charm.charm_url if not charm.is_absolute() else charm.path |
65 | ) |
66 | |
67 | @@ -343,5 +343,5 @@ |
68 | # Finally expose things |
69 | for svc in self.deployment.get_services(): |
70 | if svc.expose: |
71 | - self.log.info(" Exposing service %r" % svc.name) |
72 | + self.log.info(" Exposing application %r" % svc.name) |
73 | self.env.expose(svc.name) |
74 | |
75 | === modified file 'deployer/charm.py' |
76 | --- deployer/charm.py 2016-05-07 23:48:30 +0000 |
77 | +++ deployer/charm.py 2016-07-29 13:47:45 +0000 |
78 | @@ -96,7 +96,8 @@ |
79 | |
80 | if store_url and branch: |
81 | cls.log.error( |
82 | - "Service: %s has both charm url: %s and branch: %s specified", |
83 | + 'Application: %s has both charm url: %s and ' |
84 | + 'branch: %s specified', |
85 | name, store_url, branch) |
86 | if not store_url: |
87 | build = data.get('build', '') |
88 | |
89 | === modified file 'deployer/cli.py' |
90 | --- deployer/cli.py 2016-05-07 23:02:56 +0000 |
91 | +++ deployer/cli.py 2016-07-29 13:47:45 +0000 |
92 | @@ -47,14 +47,14 @@ |
93 | '-l', '--ls', help='List available deployments', |
94 | dest="list_deploys", action="store_true", default=False) |
95 | parser.add_argument( |
96 | - '-D', '--destroy-services', |
97 | - help='Destroy all services (do not terminate machines)', |
98 | - dest="destroy_services", action="store_true", |
99 | + '-D', '--destroy-applications', |
100 | + help='Destroy all applications (do not terminate machines)', |
101 | + dest="destroy_applications", action="store_true", |
102 | default=False) |
103 | parser.add_argument( |
104 | '-T', '--terminate-machines', |
105 | help=('Terminate all machines but the bootstrap node. ' |
106 | - 'Destroy any services that exist on each. ' |
107 | + 'Destroy any applications that exist on each. ' |
108 | 'Use -TT to forcefully terminate.'), |
109 | dest="terminate_machines", action="count", default=0) |
110 | parser.add_argument( |
111 | @@ -62,9 +62,9 @@ |
112 | help='Timeout (sec) for entire deployment (45min default)', |
113 | dest='timeout', action='store', type=int, default=2700) |
114 | parser.add_argument( |
115 | - "-f", '--find-service', action="store", type=str, |
116 | - help='Find hostname from first unit of a specific service.', |
117 | - dest="find_service") |
118 | + "-f", '--find-application', action="store", type=str, |
119 | + help='Find hostname from first unit of a specific application.', |
120 | + dest="find_application") |
121 | parser.add_argument( |
122 | "-b", '--branch-only', action="store_true", |
123 | help='Update vcs branches and exit.', |
124 | @@ -93,7 +93,7 @@ |
125 | parser.add_argument( |
126 | '-o', '--override', action='append', type=str, |
127 | help=('Override *all* config options of the same name ' |
128 | - 'across all services. Input as key=value.'), |
129 | + 'across all applications. Input as key=value.'), |
130 | dest='overrides', default=None) |
131 | parser.add_argument( |
132 | '--series', type=str, |
133 | @@ -126,7 +126,7 @@ |
134 | parser.add_argument( |
135 | '-n', '--no-relations', |
136 | default=False, dest='no_relations', action='store_true', |
137 | - help=('Do not add relations to environment, just services/units ' |
138 | + help=('Do not add relations to environment, just applications/units ' |
139 | '(default: False)')) |
140 | parser.add_argument("--description", help=argparse.SUPPRESS, |
141 | action="store_true") |
142 | @@ -181,8 +181,8 @@ |
143 | |
144 | config = ConfigStack(options.configs or [], options.series) |
145 | |
146 | - # Destroy services and exit |
147 | - if options.destroy_services or options.terminate_machines: |
148 | + # Destroy applications and exit |
149 | + if options.destroy_applications or options.terminate_machines: |
150 | log.info("Resetting environment...") |
151 | env.connect() |
152 | env.reset(terminate_machines=bool(options.terminate_machines), |
153 | @@ -192,17 +192,19 @@ |
154 | log.info("Environment reset in %0.2f", time.time() - start_time) |
155 | sys.exit(0) |
156 | |
157 | - # Display service info and exit |
158 | - if options.find_service: |
159 | - address = env.get_service_address(options.find_service) |
160 | + # Display application info and exit |
161 | + if options.find_application: |
162 | + address = env.get_service_address(options.find_application) |
163 | if address is None: |
164 | - log.error("Service not found %r", options.find_service) |
165 | + log.error("Application not found %r", options.find_application) |
166 | sys.exit(1) |
167 | elif not address: |
168 | - log.warning("Service: %s has no address for first unit", |
169 | - options.find_service) |
170 | + log.warning("Application: %s has no address for first unit", |
171 | + options.find_application) |
172 | else: |
173 | - log.info("Service: %s address: %s", options.find_service, address) |
174 | + log.info( |
175 | + "Application: %s address: %s", |
176 | + options.find_application, address) |
177 | print(address) |
178 | sys.exit(0) |
179 | |
180 | |
181 | === modified file 'deployer/config.py' |
182 | --- deployer/config.py 2016-05-07 23:48:30 +0000 |
183 | +++ deployer/config.py 2016-07-29 13:47:45 +0000 |
184 | @@ -11,7 +11,12 @@ |
185 | from six.moves.urllib.parse import urlparse |
186 | |
187 | from .deployment import Deployment |
188 | -from .utils import ErrorExit, yaml_load, path_exists, dict_merge |
189 | +from .utils import ( |
190 | + ErrorExit, |
191 | + yaml_load, |
192 | + path_exists, |
193 | + dict_merge, |
194 | +) |
195 | |
196 | |
197 | class ConfigStack(object): |
198 | @@ -54,7 +59,8 @@ |
199 | |
200 | # Check if this is a v4 bundle. |
201 | services = yaml_result.get('services') |
202 | - if isinstance(services, dict) and 'services' not in services: |
203 | + if (isinstance(services, dict) and |
204 | + 'services' not in services): |
205 | self.version = 4 |
206 | yaml_result = {config_file: yaml_result} |
207 | |
208 | |
209 | === modified file 'deployer/env/base.py' |
210 | --- deployer/env/base.py 2016-05-08 02:59:17 +0000 |
211 | +++ deployer/env/base.py 2016-07-29 13:47:45 +0000 |
212 | @@ -2,8 +2,14 @@ |
213 | import logging |
214 | |
215 | from ..utils import ( |
216 | - yaml_load, ErrorExit, |
217 | - yaml_dump, temp_file, _check_call, get_juju_major_version) |
218 | + AlternateKeyDict, |
219 | + ErrorExit, |
220 | + yaml_load, |
221 | + yaml_dump, |
222 | + temp_file, |
223 | + _check_call, |
224 | + get_juju_major_version, |
225 | +) |
226 | |
227 | |
228 | class BaseEnvironment(object): |
229 | @@ -34,17 +40,17 @@ |
230 | except KeyError: |
231 | # 'agent-state' has been removed for new versions of Juju. Respond |
232 | # with the closest status parameter. |
233 | - return entity['jujustatus']['Current'] |
234 | + return entity['agent-status']['status'] |
235 | |
236 | def _get_units_in_error(self, status=None): |
237 | units = [] |
238 | if status is None: |
239 | status = self.status() |
240 | for s in status.get('services', {}).keys(): |
241 | - for uid, u in status['services'][s].get('units', {}).items(): |
242 | + for uid, u in (status['services'][s].get('units') or {}).items(): |
243 | if 'error' in self._get_agent_state(u): |
244 | units.append(uid) |
245 | - for uid, u in u.get('subordinates', {}).items(): |
246 | + for uid, u in (u.get('subordinates') or {}).items(): |
247 | if 'error' in self._get_agent_state(u): |
248 | units.append(uid) |
249 | return units |
250 | @@ -91,20 +97,20 @@ |
251 | |
252 | params.extend([charm_url, name]) |
253 | self._check_call( |
254 | - params, self.log, "Error deploying service %r", name) |
255 | + params, self.log, "Error deploying application %r", name) |
256 | |
257 | def expose(self, name): |
258 | params = self._named_env(["juju", "expose", name]) |
259 | self._check_call( |
260 | - params, self.log, "Error exposing service %r", name) |
261 | + params, self.log, "Error exposing application %r", name) |
262 | |
263 | def terminate_machine(self, mid, wait=False, force=False): |
264 | """Terminate a machine. |
265 | |
266 | Unless ``force=True``, the machine can't have any running units. |
267 | - After removing the units or destroying the service, use wait_for_units |
268 | - to know when its safe to delete the machine (i.e., units have finished |
269 | - executing stop hooks and are removed). |
270 | + After removing the units or destroying the application, use |
271 | + wait_for_units to know when its safe to delete the machine (i.e., |
272 | + units have finished executing stop hooks and are removed). |
273 | |
274 | """ |
275 | if ((isinstance(mid, int) and mid == 0) or |
276 | @@ -130,21 +136,22 @@ |
277 | def get_service_address(self, svc_name): |
278 | status = self.get_cli_status() |
279 | if svc_name not in status['services']: |
280 | - self.log.warning("Service %s does not exist", svc_name) |
281 | + self.log.warning("Application %s does not exist", svc_name) |
282 | return None |
283 | svc = status['services'][svc_name] |
284 | if 'subordinate-to' in svc: |
285 | ps = svc['subordinate-to'][0] |
286 | self.log.info( |
287 | - 'Service %s is a subordinate to %s, finding principle service' |
288 | + 'Application %s is a subordinate to %s, ' |
289 | + 'finding principle application' |
290 | % (svc_name, ps)) |
291 | return self.get_service_address(svc['subordinate-to'][0]) |
292 | |
293 | - units = svc.get('units', {}) |
294 | + units = svc.get('units') or {} |
295 | unit_keys = list(sorted(units.keys())) |
296 | if unit_keys: |
297 | return units[unit_keys[0]].get('public-address', '') |
298 | - self.log.warning("Service %s has no units" % svc_name) |
299 | + self.log.warning("Application %s has no units" % svc_name) |
300 | |
301 | def get_cli_status(self): |
302 | params = self._named_env(["juju", "status", "--format=yaml"]) |
303 | @@ -153,10 +160,21 @@ |
304 | params, self.log, "Error getting status, is it bootstrapped?", |
305 | stderr=fh) |
306 | status = yaml_load(output) |
307 | - return status |
308 | + return NormalizedStatus(status) |
309 | |
310 | def add_unit(self, service_name, machine_spec): |
311 | raise NotImplementedError() |
312 | |
313 | def set_annotation(self, entity, annotations, entity_type='service'): |
314 | raise NotImplementedError() |
315 | + |
316 | + |
317 | +class NormalizedStatus(AlternateKeyDict): |
318 | + alternates = { |
319 | + 'services': ('Services', 'applications'), |
320 | + 'machines': ('Machines',), |
321 | + 'units': ('Units',), |
322 | + 'relations': ('Relations',), |
323 | + 'subordinates': ('Subordinates',), |
324 | + 'agent-state': ('AgentState',), |
325 | + } |
326 | |
327 | === modified file 'deployer/env/go.py' |
328 | --- deployer/env/go.py 2016-05-07 23:48:30 +0000 |
329 | +++ deployer/env/go.py 2016-07-29 13:47:45 +0000 |
330 | @@ -2,21 +2,25 @@ |
331 | from functools import cmp_to_key |
332 | import time |
333 | |
334 | -from .base import BaseEnvironment |
335 | +from .base import ( |
336 | + BaseEnvironment, |
337 | + NormalizedStatus, |
338 | +) |
339 | + |
340 | from ..utils import ( |
341 | ErrorExit, |
342 | get_juju_major_version, |
343 | parse_constraints, |
344 | ) |
345 | |
346 | -from jujuclient import ( |
347 | +from jujuclient.exc import ( |
348 | EnvError, |
349 | - Environment as EnvironmentClient, |
350 | UnitErrors, |
351 | ) |
352 | |
353 | from .watchers import ( |
354 | raise_on_errors, |
355 | + NormalizedDelta, |
356 | WaitForMachineTermination, |
357 | WaitForUnits, |
358 | ) |
359 | @@ -31,6 +35,14 @@ |
360 | self.client = None |
361 | self.juju_version = get_juju_major_version() |
362 | |
363 | + @property |
364 | + def client_class(self): |
365 | + if self.juju_version == 1: |
366 | + from jujuclient.juju1.environment import Environment |
367 | + else: |
368 | + from jujuclient.juju2.environment import Environment |
369 | + return Environment |
370 | + |
371 | def add_machine(self, series="", constraints={}): |
372 | """Add a top level machine to the Juju environment. |
373 | |
374 | @@ -41,9 +53,12 @@ |
375 | constraints: a map of constraints (such as mem, arch, etc.) which |
376 | can be parsed by utils.parse_constraints |
377 | """ |
378 | - return self.client.add_machine( |
379 | + result = self.client.add_machine( |
380 | series=series, |
381 | - constraints=parse_constraints(constraints))['Machine'] |
382 | + constraints=parse_constraints(constraints)) |
383 | + if 'Machine' in result: |
384 | + return result['Machine'] |
385 | + return result['machine'] |
386 | |
387 | def add_unit(self, service_name, machine_spec): |
388 | return self.client.add_unit(service_name, machine_spec) |
389 | @@ -70,7 +85,7 @@ |
390 | self.log, "Error getting env api endpoints, env bootstrapped?", |
391 | stderr=fh) |
392 | |
393 | - self.client = EnvironmentClient.connect(self.name) |
394 | + self.client = self.client_class.connect(self.name) |
395 | self.log.debug("Connected to environment") |
396 | |
397 | def get_config(self, svc_name): |
398 | @@ -80,7 +95,7 @@ |
399 | try: |
400 | return self.client.get_constraints(svc_name) |
401 | except EnvError as err: |
402 | - if 'constraints do not apply to subordinate services' in str(err): |
403 | + if 'constraints do not apply to subordinate' in str(err): |
404 | return {} |
405 | raise |
406 | |
407 | @@ -97,7 +112,7 @@ |
408 | status = self.status() |
409 | destroyed = False |
410 | for s in status.get('services', {}).keys(): |
411 | - self.log.debug(" Destroying service %s", s) |
412 | + self.log.debug(" Destroying application %s", s) |
413 | self.client.destroy_service(s) |
414 | destroyed = True |
415 | |
416 | @@ -137,7 +152,7 @@ |
417 | containers = set() |
418 | |
419 | def machine_sort(x, y): |
420 | - for ctype in ('lxc', 'kvm'): |
421 | + for ctype in ('lxc', 'lxd', 'kvm'): |
422 | for m in (x, y): |
423 | if ctype in m: |
424 | container_hosts.add(m.split('/', 1)[0]) |
425 | @@ -230,15 +245,16 @@ |
426 | def set_annotation(self, entity_name, annotation, entity_type='service'): |
427 | """Set an annotation on an entity. |
428 | |
429 | - entity_name: the name of the entity (machine, service, etc.) to |
430 | - annotate. |
431 | + entity_name: the name of the entity (machine, application, etc.) to |
432 | + annotate. |
433 | annotation: a dict of key/value pairs to set on the entity. |
434 | - entity_type: the type of entity (machine, service, etc.) to annotate. |
435 | + entity_type: the type of entity (machine, application, etc.) to |
436 | + annotate. |
437 | """ |
438 | return self.client.set_annotation(entity_name, entity_type, annotation) |
439 | |
440 | def status(self): |
441 | - return self.client.get_stat() |
442 | + return NormalizedStatus(self.client.status()) |
443 | |
444 | def log_errors(self, errors): |
445 | """Log the given unit errors. |
446 | @@ -246,6 +262,14 @@ |
447 | This can be used in the WaitForUnits error handling machinery, e.g. |
448 | see deployer.watchers.log_on_errors. |
449 | """ |
450 | + for e in errors: |
451 | + if 'StatusInfo' not in e: |
452 | + # convert from juju2 format |
453 | + e['Name'] = e['name'] |
454 | + e['MachineId'] = e['machine-id'] |
455 | + e['Status'] = e['workload-status']['current'] |
456 | + e['StatusInfo'] = e['workload-status']['message'] |
457 | + |
458 | messages = [ |
459 | 'unit: {Name}: machine: {MachineId} agent-state: {Status} ' |
460 | 'details: {StatusInfo}'.format(**error) for error in errors |
461 | @@ -266,8 +290,14 @@ |
462 | |
463 | def _delta_event_log(self, et, ct, d): |
464 | # event type, change type, data |
465 | + d = NormalizedDelta(d) |
466 | name = d.get('Name', d.get('Id', 'unknown')) |
467 | - state = d.get('Status', d.get('Life', 'unknown')) |
468 | + state = ( |
469 | + d.get('workload-status') or |
470 | + d.get('Status') or |
471 | + d.get('Life') or |
472 | + 'unknown' |
473 | + ) |
474 | if et == "relation": |
475 | name = self._format_endpoints(d['Endpoints']) |
476 | state = "created" |
477 | |
478 | === modified file 'deployer/env/gui.py' |
479 | --- deployer/env/gui.py 2016-05-03 16:03:18 +0000 |
480 | +++ deployer/env/gui.py 2016-07-29 13:47:45 +0000 |
481 | @@ -4,7 +4,7 @@ |
482 | See <https://code.launchpad.net/~juju-gui/charms/precise/juju-gui/trunk>. |
483 | """ |
484 | |
485 | -from .go import GoEnvironment, EnvironmentClient |
486 | +from .go import GoEnvironment |
487 | from ..utils import get_qualified_charm_url, parse_constraints |
488 | |
489 | |
490 | @@ -27,7 +27,7 @@ |
491 | client is already connected. |
492 | """ |
493 | if self.client is None: |
494 | - self.client = EnvironmentClient(self.api_endpoint) |
495 | + self.client = self.client_class(self.api_endpoint) |
496 | self.client.login(self._password, user=self._username) |
497 | |
498 | def close(self): |
499 | |
500 | === modified file 'deployer/env/mem.py' |
501 | --- deployer/env/mem.py 2016-05-07 23:48:30 +0000 |
502 | +++ deployer/env/mem.py 2016-07-29 13:47:45 +0000 |
503 | @@ -1,7 +1,9 @@ |
504 | from __future__ import absolute_import |
505 | from deployer.utils import parse_constraints |
506 | -from jujuclient import (UnitErrors, |
507 | - EnvError) |
508 | +from jujuclient.exc import ( |
509 | + UnitErrors, |
510 | + EnvError, |
511 | +) |
512 | from six.moves import range |
513 | |
514 | |
515 | |
516 | === modified file 'deployer/env/watchers.py' |
517 | --- deployer/env/watchers.py 2016-05-07 23:48:30 +0000 |
518 | +++ deployer/env/watchers.py 2016-07-29 13:47:45 +0000 |
519 | @@ -1,15 +1,34 @@ |
520 | """A collection of juju-core environment watchers.""" |
521 | |
522 | from __future__ import absolute_import |
523 | -from jujuclient import WatchWrapper |
524 | +from jujuclient.watch import WatchWrapper |
525 | |
526 | -from ..utils import ErrorExit |
527 | +from ..utils import ( |
528 | + AlternateKeyDict, |
529 | + ErrorExit, |
530 | +) |
531 | |
532 | # _status_map provides a translation of Juju 2 status codes to the closest |
533 | # Juju 1 equivalent. Only defines codes that need translation. |
534 | _status_map = {'idle': 'started'} |
535 | |
536 | |
537 | +class NormalizedDelta(AlternateKeyDict): |
538 | + alternates = { |
539 | + 'Name': ('name',), |
540 | + 'Id': ('id',), |
541 | + 'Status': ('status',), |
542 | + 'JujuStatus': ('juju-status',), |
543 | + 'Current': ('current',), |
544 | + 'Life': ('life',), |
545 | + 'Endpoints': ('endpoints',), |
546 | + 'Service': ('service', 'application'), |
547 | + 'ServiceName': ('application-name',), |
548 | + 'Relation': ('relation',), |
549 | + 'Role': ('role',), |
550 | + } |
551 | + |
552 | + |
553 | class WaitForMachineTermination(WatchWrapper): |
554 | """Wait until the given machines are terminated.""" |
555 | |
556 | @@ -21,6 +40,8 @@ |
557 | def process(self, entity_type, change, data): |
558 | if entity_type != 'machine': |
559 | return |
560 | + |
561 | + data = NormalizedDelta(data) |
562 | if change == 'remove' and data['Id'] in self.machines: |
563 | self.machines.remove(data['Id']) |
564 | else: |
565 | @@ -57,6 +78,8 @@ |
566 | def process(self, entity, action, data): |
567 | if entity != 'unit': |
568 | return |
569 | + |
570 | + data = NormalizedDelta(data) |
571 | if (self.services is None) or (data['Service'] in self.services): |
572 | unit_name = data['Name'] |
573 | if action == 'remove' and unit_name in self.units: |
574 | @@ -72,19 +95,21 @@ |
575 | units_in_error = self.units_in_error |
576 | for unit_name, data in self.units.items(): |
577 | try: |
578 | - status = data['Status'] |
579 | + err_status = data['Status'] |
580 | + goal_status = err_status |
581 | except KeyError: |
582 | # 'Status' has been removed from newer versions of Juju. |
583 | # Respond with the closest status parameter, translating it |
584 | # through the _status_map. If the status value is not in |
585 | # _status_map, just use the original value. |
586 | - status = data['JujuStatus']['Current'] |
587 | - status = _status_map.get(status, status) |
588 | - if status == 'error': |
589 | + err_status = data['workload-status']['current'] |
590 | + goal_status = data['agent-status']['current'] |
591 | + goal_status = _status_map.get(goal_status, goal_status) |
592 | + if err_status == 'error': |
593 | if unit_name not in units_in_error: |
594 | units_in_error.append(unit_name) |
595 | new_errors.append(data) |
596 | - elif status != goal_state: |
597 | + elif goal_status != goal_state: |
598 | ready = False |
599 | if new_errors and goal_state != 'removed' and callable(on_errors): |
600 | on_errors(new_errors) |
601 | |
602 | === modified file 'deployer/errors.py' |
603 | --- deployer/errors.py 2016-05-07 23:48:30 +0000 |
604 | +++ deployer/errors.py 2016-07-29 13:47:45 +0000 |
605 | @@ -1,4 +1,4 @@ |
606 | # TODO make deployer specific exceptions, |
607 | # also move errorexit from utils to here. |
608 | from __future__ import absolute_import |
609 | -from jujuclient import UnitErrors, EnvError # noqa |
610 | +from jujuclient.exc import UnitErrors, EnvError # noqa |
611 | |
612 | === modified file 'deployer/service.py' |
613 | --- deployer/service.py 2016-05-12 20:22:06 +0000 |
614 | +++ deployer/service.py 2016-07-29 13:47:45 +0000 |
615 | @@ -2,8 +2,25 @@ |
616 | import itertools |
617 | |
618 | from .feedback import Feedback |
619 | +from .utils import get_juju_major_version |
620 | from six.moves import map |
621 | |
622 | +# Map of container-type-used-in-bundle to actual-container-type-passed-to-juju. |
623 | +if get_juju_major_version() == 1: |
624 | + # lxd not supported on juju1 |
625 | + CONTAINER_TYPES = { |
626 | + 'lxc': 'lxc', |
627 | + 'kvm': 'kvm', |
628 | + } |
629 | +else: |
630 | + # If you use 'lxc' in a bundle with juju2, deployer will translate it to |
631 | + # 'lxd' before passing the deploy command to juju. |
632 | + CONTAINER_TYPES = { |
633 | + 'lxc': 'lxd', |
634 | + 'lxd': 'lxd', |
635 | + 'kvm': 'kvm', |
636 | + } |
637 | + |
638 | |
639 | class Service(object): |
640 | |
641 | @@ -82,8 +99,8 @@ |
642 | # Should be caught in validate relations but sanity check |
643 | # for concurrency. |
644 | self.deployment.log.error( |
645 | - "Service %s to be deployed with non-existent service %s", |
646 | - svc.name, placement) |
647 | + "Application %s to be deployed with non-existent " |
648 | + "application %s", svc.name, placement) |
649 | # Prefer continuing deployment with a new machine rather |
650 | # than an in-progress abort. |
651 | return None |
652 | @@ -91,7 +108,8 @@ |
653 | svc_units = with_service['units'] |
654 | if int(u_idx) >= len(svc_units): |
655 | self.deployment.log.warning( |
656 | - "Service:%s, Deploy-with-service:%s, Requested-unit-index=%s, " |
657 | + "Application:%s, Deploy-with-application:%s, " |
658 | + "Requested-unit-index=%s, " |
659 | "Cannot solve, falling back to default placement", |
660 | svc.name, placement, u_idx) |
661 | return None |
662 | @@ -100,7 +118,7 @@ |
663 | machine = svc_units[unit_names[int(u_idx)]].get('machine') |
664 | if not machine: |
665 | self.deployment.log.warning( |
666 | - "Service:%s deploy-with unit missing machine %s", |
667 | + "Application:%s deploy-with unit missing machine %s", |
668 | svc.name, unit_names[int(u_idx)]) |
669 | return None |
670 | return self._format_placement(machine, container) |
671 | @@ -134,23 +152,23 @@ |
672 | |
673 | for idx, p in enumerate(unit_placement): |
674 | container, p, u_idx = self._parse_placement(p) |
675 | - if container and container not in ('lxc', 'kvm'): |
676 | + if container and container not in CONTAINER_TYPES: |
677 | feedback.error( |
678 | - "Invalid container type:%s service: %s placement: %s" |
679 | + "Invalid container type:%s application: %s placement: %s" |
680 | % (container, self.service.name, unit_placement[idx])) |
681 | if u_idx: |
682 | if p in ('maas', 'zone'): |
683 | continue |
684 | if not u_idx.isdigit(): |
685 | feedback.error( |
686 | - "Invalid service:%s placement: %s" % ( |
687 | + "Invalid application:%s placement: %s" % ( |
688 | self.service.name, unit_placement[idx])) |
689 | if p.isdigit(): |
690 | if p == '0' or p in machines or self.arbitrary_machines: |
691 | continue |
692 | else: |
693 | feedback.error( |
694 | - ("Service placement to machine " |
695 | + ("Application placement to machine " |
696 | "not supported %s to %s") % ( |
697 | self.service.name, unit_placement[idx])) |
698 | elif p in services: |
699 | @@ -160,11 +178,11 @@ |
700 | self.service.name, p, services[p].unit_placement)) |
701 | elif self.deployment.get_charm_for(p).is_subordinate(): |
702 | feedback.error( |
703 | - "Cannot place to a subordinate service: %s -> %s" % ( |
704 | - self.service.name, p)) |
705 | + "Cannot place to a subordinate application: " |
706 | + "%s -> %s" % (self.service.name, p)) |
707 | else: |
708 | feedback.error( |
709 | - "Invalid service placement %s to %s" % ( |
710 | + "Invalid application placement %s to %s" % ( |
711 | self.service.name, unit_placement[idx])) |
712 | return feedback |
713 | |
714 | @@ -188,6 +206,7 @@ |
715 | |
716 | if ':' in unit_placement: |
717 | container, placement = unit_placement.split(":") |
718 | + container = CONTAINER_TYPES[container] |
719 | if '=' in placement: |
720 | placement, u_idx = placement.split("=") |
721 | |
722 | @@ -222,14 +241,15 @@ |
723 | """Fill the placement spec with necessary data. |
724 | |
725 | From the spec: |
726 | - A unit placement may be specified with a service name only, in which |
727 | - case its unit number is assumed to be one more than the unit number of |
728 | - the previous unit in the list with the same service, or zero if there |
729 | - were none. |
730 | + A unit placement may be specified with an application name only, |
731 | + in which case its unit number is assumed to be one more than the |
732 | + unit number of the previous unit in the list with the same |
733 | + application, or zero if there were none. |
734 | |
735 | If there are less elements in To than NumUnits, the last element is |
736 | replicated to fill it. If there are no elements (or To is omitted), |
737 | "new" is replicated. |
738 | + |
739 | """ |
740 | unit_mapping = self.service.unit_placement |
741 | unit_count = self.service.num_units |
742 | @@ -264,24 +284,27 @@ |
743 | |
744 | This splits the placement into a container, a placement, and a unit |
745 | number. Both container and unit number are optional and can be None. |
746 | + |
747 | """ |
748 | container = unit_number = None |
749 | if ':' in placement: |
750 | container, placement = placement.split(':') |
751 | + container = CONTAINER_TYPES.get(container, container) |
752 | if '/' in placement: |
753 | placement, unit_number = placement.split('/') |
754 | return container, placement, unit_number |
755 | |
756 | def validate(self): |
757 | - """Validate the placement of a service and all of its units. |
758 | + """Validate the placement of an application and all of its units. |
759 | |
760 | - If a service has a 'to' block specified, the list of machines, units, |
761 | - containers, and/or services must be internally consistent, consistent |
762 | - with other services in the deployment, and consistent with any machines |
763 | - specified in the 'machines' block of the deployment. |
764 | + If an application has a 'to' block specified, the list of machines, |
765 | + units, containers, and/or applications must be internally consistent, |
766 | + consistent with other applications in the deployment, and consistent |
767 | + with any machines specified in the 'machines' block of the deployment. |
768 | |
769 | A feedback object is returned, potentially with errors and warnings |
770 | inside it. |
771 | + |
772 | """ |
773 | feedback = Feedback() |
774 | |
775 | @@ -301,9 +324,9 @@ |
776 | container, target, unit_number = self._parse_placement(placement) |
777 | |
778 | # Validate the container type. |
779 | - if container and container not in ('lxc', 'kvm'): |
780 | + if container and container not in CONTAINER_TYPES: |
781 | feedback.error( |
782 | - 'Invalid container type: {} service: {} placement: {}' |
783 | + 'Invalid container type: {} application: {} placement: {}' |
784 | ''.format(container, service_name, placement)) |
785 | # Specify an existing machine (or, if the number is in the |
786 | # list of machine specs, one of those). |
787 | @@ -311,7 +334,7 @@ |
788 | continue |
789 | if target.isdigit(): |
790 | feedback.error( |
791 | - 'Service placement to machine not supported: {} to {}' |
792 | + 'Application placement to machine not supported: {} to {}' |
793 | ''.format(service_name, placement)) |
794 | # Specify a service for co-location. |
795 | elif target in services: |
796 | @@ -326,24 +349,24 @@ |
797 | continue |
798 | if unit_number > services[target].num_units: |
799 | feedback.error( |
800 | - 'Service unit does not exist: {} to {}/{}' |
801 | + 'Application unit does not exist: {} to {}/{}' |
802 | ''.format(service_name, target, unit_number)) |
803 | continue |
804 | if self.deployment.get_charm_for(target).is_subordinate(): |
805 | feedback.error( |
806 | - 'Cannot place to a subordinate service: {} -> {}' |
807 | + 'Cannot place to a subordinate application: {} -> {}' |
808 | ''.format(service_name, target)) |
809 | # Create a new machine or container. |
810 | elif target == 'new': |
811 | continue |
812 | else: |
813 | feedback.error( |
814 | - 'Invalid service placement: {} to {}' |
815 | + 'Invalid application placement: {} to {}' |
816 | ''.format(service_name, placement)) |
817 | return feedback |
818 | |
819 | def get_new_machines_for_containers(self): |
820 | - """Return a list of containers in the service's unit placement that |
821 | + """Return a list of containers in the application's unit placement that |
822 | have been requested to be put on new machines.""" |
823 | new_machines = [] |
824 | unit = itertools.count() |
825 | |
826 | === modified file 'deployer/tests/test_deployment.py' |
827 | --- deployer/tests/test_deployment.py 2016-05-08 01:11:12 +0000 |
828 | +++ deployer/tests/test_deployment.py 2016-07-29 13:47:45 +0000 |
829 | @@ -6,6 +6,7 @@ |
830 | |
831 | from deployer.deployment import Deployment |
832 | from deployer.utils import setup_logging, ErrorExit |
833 | +from deployer.service import CONTAINER_TYPES |
834 | |
835 | from .base import Base, skip_if_offline |
836 | |
837 | @@ -151,18 +152,18 @@ |
838 | |
839 | self.assertEqual(p.colocate(status, 'asdf', '1', '', svc), |
840 | None) |
841 | - self.assertIn('Service bar to be deployed with non-existent service ' |
842 | - 'asdf', |
843 | + self.assertIn('Application bar to be deployed with non-existent ' |
844 | + 'application asdf', |
845 | self.output.getvalue()) |
846 | self.assertEqual(p.colocate(status, 'foo', '2', '', svc), |
847 | None) |
848 | - self.assertIn('Service:bar, Deploy-with-service:foo, ' |
849 | + self.assertIn('Application:bar, Deploy-with-application:foo, ' |
850 | 'Requested-unit-index=2, Cannot solve, ' |
851 | 'falling back to default placement', |
852 | self.output.getvalue()) |
853 | self.assertEqual(p.colocate(status, 'foo', '1', '', svc), |
854 | None) |
855 | - self.assertIn('Service:bar deploy-with unit missing machine 2', |
856 | + self.assertIn('Application:bar deploy-with unit missing machine 2', |
857 | self.output.getvalue()) |
858 | self.assertEqual(p.colocate(status, 'foo', '0', '', svc), 1) |
859 | |
860 | @@ -201,9 +202,11 @@ |
861 | deployment.validate_placement() |
862 | output = self.output.getvalue() |
863 | self.assertIn( |
864 | - 'Cannot place to a subordinate service: ceph -> nrpe\n', output) |
865 | + 'Cannot place to a subordinate application: ' |
866 | + 'ceph -> nrpe\n', output) |
867 | self.assertIn( |
868 | - 'Cannot place to a subordinate service: nova-compute -> nrpe\n', |
869 | + 'Cannot place to a subordinate application: ' |
870 | + 'nova-compute -> nrpe\n', |
871 | output) |
872 | |
873 | @skip_if_offline |
874 | @@ -215,7 +218,8 @@ |
875 | deployment.validate_placement() |
876 | output = self.output.getvalue() |
877 | self.assertIn( |
878 | - 'Cannot place to a subordinate service: nova-compute -> nrpe\n', |
879 | + 'Cannot place to a subordinate application: ' |
880 | + 'nova-compute -> nrpe\n', |
881 | output) |
882 | |
883 | def test_validate_invalid_unit_number(self): |
884 | @@ -228,6 +232,12 @@ |
885 | 'Invalid unit number for placement: django to bad-wolf\n', output) |
886 | |
887 | def test_get_unit_placement_v3(self): |
888 | + def _container(s): |
889 | + if ':' in s: |
890 | + typ, num = s.split(':') |
891 | + return '{}:{}'.format(CONTAINER_TYPES[typ], num) |
892 | + return CONTAINER_TYPES[typ] |
893 | + |
894 | d = self.get_named_deployment_v3("stack-placement.yaml", "stack") |
895 | status = { |
896 | 'services': { |
897 | @@ -242,12 +252,12 @@ |
898 | self.assertEqual(placement.get(2), None) |
899 | |
900 | placement = d.get_unit_placement('quantum', status) |
901 | - self.assertEqual(placement.get(0), 'lxc:1') |
902 | - self.assertEqual(placement.get(2), 'lxc:3') |
903 | + self.assertEqual(placement.get(0), _container('lxc:1')) |
904 | + self.assertEqual(placement.get(2), _container('lxc:3')) |
905 | self.assertEqual(placement.get(3), None) |
906 | |
907 | placement = d.get_unit_placement('verity', status) |
908 | - self.assertEqual(placement.get(0), 'lxc:3') |
909 | + self.assertEqual(placement.get(0), _container('lxc:3')) |
910 | |
911 | placement = d.get_unit_placement('mysql', status) |
912 | self.assertEqual(placement.get(0), '0') |
913 | @@ -256,11 +266,11 @@ |
914 | self.assertEqual(placement.get(0), '3') |
915 | |
916 | placement = d.get_unit_placement('lxc-service', status) |
917 | - self.assertEqual(placement.get(0), 'lxc:2') |
918 | - self.assertEqual(placement.get(1), 'lxc:3') |
919 | - self.assertEqual(placement.get(2), 'lxc:1') |
920 | - self.assertEqual(placement.get(3), 'lxc:1') |
921 | - self.assertEqual(placement.get(4), 'lxc:3') |
922 | + self.assertEqual(placement.get(0), _container('lxc:2')) |
923 | + self.assertEqual(placement.get(1), _container('lxc:3')) |
924 | + self.assertEqual(placement.get(2), _container('lxc:1')) |
925 | + self.assertEqual(placement.get(3), _container('lxc:1')) |
926 | + self.assertEqual(placement.get(4), _container('lxc:3')) |
927 | |
928 | def test_fill_placement_v4(self): |
929 | d = self.get_deployment_v4('fill_placement.yaml') |
930 | @@ -290,7 +300,7 @@ |
931 | self.assertEqual(u, '1') |
932 | |
933 | c, p, u = placement._parse_placement('lxc:mysql') |
934 | - self.assertEqual(c, 'lxc') |
935 | + self.assertEqual(c, CONTAINER_TYPES['lxc']) |
936 | self.assertEqual(p, 'mysql') |
937 | self.assertEqual(u, None) |
938 | |
939 | @@ -299,12 +309,13 @@ |
940 | placement = d.get_unit_placement('mysql', {}) |
941 | feedback = placement.validate() |
942 | self.assertEqual(feedback.get_errors(), [ |
943 | - 'Invalid container type: asdf service: mysql placement: asdf:0', |
944 | - 'Service placement to machine not supported: mysql to asdf:0', |
945 | - 'Invalid service placement: mysql to lxc:asdf', |
946 | - 'Service placement to machine not supported: mysql to 1', |
947 | - 'Service unit does not exist: mysql to wordpress/3', |
948 | - 'Invalid service placement: mysql to asdf']) |
949 | + 'Invalid container type: asdf application: mysql ' |
950 | + 'placement: asdf:0', |
951 | + 'Application placement to machine not supported: mysql to asdf:0', |
952 | + 'Invalid application placement: mysql to lxc:asdf', |
953 | + 'Application placement to machine not supported: mysql to 1', |
954 | + 'Application unit does not exist: mysql to wordpress/3', |
955 | + 'Invalid application placement: mysql to asdf']) |
956 | |
957 | def test_get_unit_placement_v4_simple(self): |
958 | d = self.get_deployment_v4('simple.yaml') |
959 | @@ -419,7 +430,10 @@ |
960 | d.set_machines(machines) |
961 | |
962 | placement = d.get_unit_placement('mysql', status) |
963 | - self.assertEqual(placement.get(0), 'lxc:1') |
964 | + self.assertEqual( |
965 | + placement.get(0), |
966 | + '{}:1'.format(CONTAINER_TYPES['lxc']) |
967 | + ) |
968 | |
969 | placement = d.get_unit_placement('mediawiki', status) |
970 | self.assertEqual(placement.get(0), 1) |
971 | @@ -446,7 +460,10 @@ |
972 | self.assertEqual( |
973 | placement.get_new_machines_for_containers(), |
974 | ['mysql/0']) |
975 | - self.assertEqual(placement.get(0), 'lxc:2') |
976 | + self.assertEqual( |
977 | + placement.get(0), |
978 | + '{}:2'.format(CONTAINER_TYPES['lxc']) |
979 | + ) |
980 | |
981 | placement = d.get_unit_placement('mediawiki', status) |
982 | self.assertEqual(placement.get(0), 1) |
983 | |
984 | === modified file 'deployer/tests/test_goenv.py' |
985 | --- deployer/tests/test_goenv.py 2016-05-07 23:02:56 +0000 |
986 | +++ deployer/tests/test_goenv.py 2016-07-29 13:47:45 +0000 |
987 | @@ -77,7 +77,7 @@ |
988 | self.assertIn(u['agent-state'], |
989 | ("allocating", "pending", "started")) |
990 | except KeyError: |
991 | - self.assertIn(u['jujustatus']['Current'], |
992 | + self.assertIn(u['agent-status']['status'], |
993 | ("allocating", "idle")) |
994 | |
995 | def test_add_machine(self): |
996 | |
997 | === modified file 'deployer/tests/test_guienv.py' |
998 | --- deployer/tests/test_guienv.py 2016-05-07 23:02:56 +0000 |
999 | +++ deployer/tests/test_guienv.py 2016-07-29 13:47:45 +0000 |
1000 | @@ -9,7 +9,7 @@ |
1001 | ) |
1002 | |
1003 | |
1004 | -@mock.patch('deployer.env.gui.EnvironmentClient') |
1005 | +@mock.patch.object(GUIEnvironment, 'client_class') |
1006 | class TestGUIEnvironment(unittest.TestCase): |
1007 | |
1008 | endpoint = 'wss://api.example.com:17070' |
1009 | |
1010 | === modified file 'deployer/tests/test_guiserver.py' |
1011 | --- deployer/tests/test_guiserver.py 2016-05-07 23:02:56 +0000 |
1012 | +++ deployer/tests/test_guiserver.py 2016-07-29 13:47:45 +0000 |
1013 | @@ -28,8 +28,8 @@ |
1014 | # When adding/modifying options, ensure the defaults are sane for us. |
1015 | expected_keys = set([ |
1016 | 'bootstrap', 'branch_only', 'configs', 'debug', 'deploy_delay', |
1017 | - 'deployment', 'description', 'destroy_services', 'diff', |
1018 | - 'find_service', 'ignore_errors', 'juju_env', 'list_deploys', |
1019 | + 'deployment', 'description', 'destroy_applications', 'diff', |
1020 | + 'find_application', 'ignore_errors', 'juju_env', 'list_deploys', |
1021 | 'no_local_mods', 'no_relations', 'overrides', 'rel_wait', |
1022 | 'retry_count', 'series', 'skip_unit_wait', 'terminate_machines', |
1023 | 'timeout', 'update_charms', 'verbose', 'watch' |
1024 | @@ -46,9 +46,9 @@ |
1025 | self.assertFalse(options.debug) |
1026 | self.assertEqual(0, options.deploy_delay) |
1027 | self.assertIsNone(options.deployment) |
1028 | - self.assertFalse(options.destroy_services) |
1029 | + self.assertFalse(options.destroy_applications) |
1030 | self.assertFalse(options.diff) |
1031 | - self.assertIsNone(options.find_service) |
1032 | + self.assertIsNone(options.find_application) |
1033 | self.assertTrue(options.ignore_errors) |
1034 | self.assertEqual(os.getenv("JUJU_ENV"), options.juju_env) |
1035 | self.assertFalse(options.list_deploys) |
1036 | |
1037 | === modified file 'deployer/utils.py' |
1038 | --- deployer/utils.py 2016-05-13 14:16:48 +0000 |
1039 | +++ deployer/utils.py 2016-07-29 13:47:45 +0000 |
1040 | @@ -427,3 +427,40 @@ |
1041 | if x.name == placement: |
1042 | return True |
1043 | return False |
1044 | + |
1045 | + |
1046 | +class AlternateKeyDict(dict): |
1047 | + def __getitem__(self, key): |
1048 | + try: |
1049 | + val = super(AlternateKeyDict, self).__getitem__(key) |
1050 | + if isinstance(val, dict): |
1051 | + return self.__class__(val) |
1052 | + return val |
1053 | + except KeyError: |
1054 | + if key not in self.alternates: |
1055 | + raise |
1056 | + |
1057 | + for alt in self.alternates[key]: |
1058 | + if alt in self: |
1059 | + return self[alt] |
1060 | + raise |
1061 | + |
1062 | + def get(self, key, default=None): |
1063 | + try: |
1064 | + return self.__getitem__(key) |
1065 | + except KeyError: |
1066 | + return default |
1067 | + |
1068 | + def values(self): |
1069 | + for val in super(AlternateKeyDict, self).values(): |
1070 | + if isinstance(val, dict): |
1071 | + yield self.__class__(val) |
1072 | + else: |
1073 | + yield val |
1074 | + |
1075 | + def items(self): |
1076 | + for key, val in super(AlternateKeyDict, self).items(): |
1077 | + if isinstance(val, dict): |
1078 | + yield key, self.__class__(val) |
1079 | + else: |
1080 | + yield key, val |
1081 | |
1082 | === modified file 'tox.ini' |
1083 | --- tox.ini 2016-05-13 12:26:48 +0000 |
1084 | +++ tox.ini 2016-07-29 13:47:45 +0000 |
1085 | @@ -15,7 +15,7 @@ |
1086 | JUJU_DATA = {homedir}/.local/share/juju |
1087 | HOME = {env:HOME} |
1088 | commands= |
1089 | - nosetests --with-coverage --cover-package=deployer deployer/tests |
1090 | + nosetests -s --with-coverage --cover-package=deployer deployer/tests |
1091 | |
1092 | [testenv:pep8] |
1093 | commands = flake8 deployer |