Merge lp:~tvansteenburgh/juju-deployer/beta11 into lp:juju-deployer

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
Reviewer Review Type Date Requested Status
juju-deployers Pending
Review via email: mp+299884@code.launchpad.net

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

Subscribers

People subscribed via source and target branches