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
=== modified file 'deployer/action/diff.py'
--- deployer/action/diff.py 2016-05-12 20:22:06 +0000
+++ deployer/action/diff.py 2016-07-29 13:47:45 +0000
@@ -32,7 +32,7 @@
32 self.env_state['services'][svc_name][32 self.env_state['services'][svc_name][
33 'constraints'] = self.env.get_constraints(svc_name)33 'constraints'] = self.env.get_constraints(svc_name)
34 self.env_state['services'][svc_name]['unit_count'] = len(34 self.env_state['services'][svc_name]['unit_count'] = len(
35 self.env_status['services'][svc_name].get('units', {}))35 (self.env_status['services'][svc_name].get('units') or {}))
36 rels.update(self._load_rels(svc_name))36 rels.update(self._load_rels(svc_name))
37 self.env_state['relations'] = sorted(rels)37 self.env_state['relations'] = sorted(rels)
3838
@@ -61,7 +61,7 @@
61 for r, eps in svc_rels.items():61 for r, eps in svc_rels.items():
62 if src in eps:62 if src in eps:
63 if found:63 if found:
64 raise ValueError("Ambigious relations for service")64 raise ValueError("Ambigious relations for application")
65 found = r65 found = r
66 return found66 return found
6767
6868
=== modified file 'deployer/action/importer.py'
--- deployer/action/importer.py 2016-05-07 23:02:56 +0000
+++ deployer/action/importer.py 2016-07-29 13:47:45 +0000
@@ -32,7 +32,7 @@
32 while delay > time.time():32 while delay > time.time():
33 if svc.name in env_status['services']:33 if svc.name in env_status['services']:
34 cur_units = len(34 cur_units = len(
35 env_status['services'][svc.name].get('units', ())35 (env_status['services'][svc.name].get('units') or ())
36 )36 )
37 if cur_units > 0:37 if cur_units > 0:
38 break38 break
@@ -43,7 +43,7 @@
4343
44 if delta <= 0:44 if delta <= 0:
45 self.log.debug(45 self.log.debug(
46 " Service %r does not need any more units added.",46 " Application %r does not need any more units added.",
47 svc.name)47 svc.name)
48 continue48 continue
4949
@@ -161,19 +161,19 @@
161 in a machine spec, and units will be placed accordingly if this161 in a machine spec, and units will be placed accordingly if this
162 flag is false.162 flag is false.
163 """163 """
164 self.log.info("Deploying services...")164 self.log.info("Deploying applications...")
165 env_status = self.env.status()165 env_status = self.env.status()
166 reloaded = False166 reloaded = False
167167
168 for svc in self.deployment.get_services():168 for svc in self.deployment.get_services():
169 if svc.name in env_status['services']:169 if svc.name in env_status['services']:
170 self.log.debug(170 self.log.debug(
171 " Service %r already deployed. Skipping" % svc.name)171 " Application %r already deployed. Skipping" % svc.name)
172 continue172 continue
173173
174 charm = self.deployment.get_charm_for(svc.name)174 charm = self.deployment.get_charm_for(svc.name)
175 self.log.info(175 self.log.info(
176 " Deploying service %s using %s", svc.name,176 " Deploying application %s using %s", svc.name,
177 charm.charm_url if not charm.is_absolute() else charm.path177 charm.charm_url if not charm.is_absolute() else charm.path
178 )178 )
179179
@@ -343,5 +343,5 @@
343 # Finally expose things343 # Finally expose things
344 for svc in self.deployment.get_services():344 for svc in self.deployment.get_services():
345 if svc.expose:345 if svc.expose:
346 self.log.info(" Exposing service %r" % svc.name)346 self.log.info(" Exposing application %r" % svc.name)
347 self.env.expose(svc.name)347 self.env.expose(svc.name)
348348
=== modified file 'deployer/charm.py'
--- deployer/charm.py 2016-05-07 23:48:30 +0000
+++ deployer/charm.py 2016-07-29 13:47:45 +0000
@@ -96,7 +96,8 @@
9696
97 if store_url and branch:97 if store_url and branch:
98 cls.log.error(98 cls.log.error(
99 "Service: %s has both charm url: %s and branch: %s specified",99 'Application: %s has both charm url: %s and '
100 'branch: %s specified',
100 name, store_url, branch)101 name, store_url, branch)
101 if not store_url:102 if not store_url:
102 build = data.get('build', '')103 build = data.get('build', '')
103104
=== modified file 'deployer/cli.py'
--- deployer/cli.py 2016-05-07 23:02:56 +0000
+++ deployer/cli.py 2016-07-29 13:47:45 +0000
@@ -47,14 +47,14 @@
47 '-l', '--ls', help='List available deployments',47 '-l', '--ls', help='List available deployments',
48 dest="list_deploys", action="store_true", default=False)48 dest="list_deploys", action="store_true", default=False)
49 parser.add_argument(49 parser.add_argument(
50 '-D', '--destroy-services',50 '-D', '--destroy-applications',
51 help='Destroy all services (do not terminate machines)',51 help='Destroy all applications (do not terminate machines)',
52 dest="destroy_services", action="store_true",52 dest="destroy_applications", action="store_true",
53 default=False)53 default=False)
54 parser.add_argument(54 parser.add_argument(
55 '-T', '--terminate-machines',55 '-T', '--terminate-machines',
56 help=('Terminate all machines but the bootstrap node. '56 help=('Terminate all machines but the bootstrap node. '
57 'Destroy any services that exist on each. '57 'Destroy any applications that exist on each. '
58 'Use -TT to forcefully terminate.'),58 'Use -TT to forcefully terminate.'),
59 dest="terminate_machines", action="count", default=0)59 dest="terminate_machines", action="count", default=0)
60 parser.add_argument(60 parser.add_argument(
@@ -62,9 +62,9 @@
62 help='Timeout (sec) for entire deployment (45min default)',62 help='Timeout (sec) for entire deployment (45min default)',
63 dest='timeout', action='store', type=int, default=2700)63 dest='timeout', action='store', type=int, default=2700)
64 parser.add_argument(64 parser.add_argument(
65 "-f", '--find-service', action="store", type=str,65 "-f", '--find-application', action="store", type=str,
66 help='Find hostname from first unit of a specific service.',66 help='Find hostname from first unit of a specific application.',
67 dest="find_service")67 dest="find_application")
68 parser.add_argument(68 parser.add_argument(
69 "-b", '--branch-only', action="store_true",69 "-b", '--branch-only', action="store_true",
70 help='Update vcs branches and exit.',70 help='Update vcs branches and exit.',
@@ -93,7 +93,7 @@
93 parser.add_argument(93 parser.add_argument(
94 '-o', '--override', action='append', type=str,94 '-o', '--override', action='append', type=str,
95 help=('Override *all* config options of the same name '95 help=('Override *all* config options of the same name '
96 'across all services. Input as key=value.'),96 'across all applications. Input as key=value.'),
97 dest='overrides', default=None)97 dest='overrides', default=None)
98 parser.add_argument(98 parser.add_argument(
99 '--series', type=str,99 '--series', type=str,
@@ -126,7 +126,7 @@
126 parser.add_argument(126 parser.add_argument(
127 '-n', '--no-relations',127 '-n', '--no-relations',
128 default=False, dest='no_relations', action='store_true',128 default=False, dest='no_relations', action='store_true',
129 help=('Do not add relations to environment, just services/units '129 help=('Do not add relations to environment, just applications/units '
130 '(default: False)'))130 '(default: False)'))
131 parser.add_argument("--description", help=argparse.SUPPRESS,131 parser.add_argument("--description", help=argparse.SUPPRESS,
132 action="store_true")132 action="store_true")
@@ -181,8 +181,8 @@
181181
182 config = ConfigStack(options.configs or [], options.series)182 config = ConfigStack(options.configs or [], options.series)
183183
184 # Destroy services and exit184 # Destroy applications and exit
185 if options.destroy_services or options.terminate_machines:185 if options.destroy_applications or options.terminate_machines:
186 log.info("Resetting environment...")186 log.info("Resetting environment...")
187 env.connect()187 env.connect()
188 env.reset(terminate_machines=bool(options.terminate_machines),188 env.reset(terminate_machines=bool(options.terminate_machines),
@@ -192,17 +192,19 @@
192 log.info("Environment reset in %0.2f", time.time() - start_time)192 log.info("Environment reset in %0.2f", time.time() - start_time)
193 sys.exit(0)193 sys.exit(0)
194194
195 # Display service info and exit195 # Display application info and exit
196 if options.find_service:196 if options.find_application:
197 address = env.get_service_address(options.find_service)197 address = env.get_service_address(options.find_application)
198 if address is None:198 if address is None:
199 log.error("Service not found %r", options.find_service)199 log.error("Application not found %r", options.find_application)
200 sys.exit(1)200 sys.exit(1)
201 elif not address:201 elif not address:
202 log.warning("Service: %s has no address for first unit",202 log.warning("Application: %s has no address for first unit",
203 options.find_service)203 options.find_application)
204 else:204 else:
205 log.info("Service: %s address: %s", options.find_service, address)205 log.info(
206 "Application: %s address: %s",
207 options.find_application, address)
206 print(address)208 print(address)
207 sys.exit(0)209 sys.exit(0)
208210
209211
=== modified file 'deployer/config.py'
--- deployer/config.py 2016-05-07 23:48:30 +0000
+++ deployer/config.py 2016-07-29 13:47:45 +0000
@@ -11,7 +11,12 @@
11from six.moves.urllib.parse import urlparse11from six.moves.urllib.parse import urlparse
1212
13from .deployment import Deployment13from .deployment import Deployment
14from .utils import ErrorExit, yaml_load, path_exists, dict_merge14from .utils import (
15 ErrorExit,
16 yaml_load,
17 path_exists,
18 dict_merge,
19)
1520
1621
17class ConfigStack(object):22class ConfigStack(object):
@@ -54,7 +59,8 @@
5459
55 # Check if this is a v4 bundle.60 # Check if this is a v4 bundle.
56 services = yaml_result.get('services')61 services = yaml_result.get('services')
57 if isinstance(services, dict) and 'services' not in services:62 if (isinstance(services, dict) and
63 'services' not in services):
58 self.version = 464 self.version = 4
59 yaml_result = {config_file: yaml_result}65 yaml_result = {config_file: yaml_result}
6066
6167
=== modified file 'deployer/env/base.py'
--- deployer/env/base.py 2016-05-08 02:59:17 +0000
+++ deployer/env/base.py 2016-07-29 13:47:45 +0000
@@ -2,8 +2,14 @@
2import logging2import logging
33
4from ..utils import (4from ..utils import (
5 yaml_load, ErrorExit,5 AlternateKeyDict,
6 yaml_dump, temp_file, _check_call, get_juju_major_version)6 ErrorExit,
7 yaml_load,
8 yaml_dump,
9 temp_file,
10 _check_call,
11 get_juju_major_version,
12)
713
814
9class BaseEnvironment(object):15class BaseEnvironment(object):
@@ -34,17 +40,17 @@
34 except KeyError:40 except KeyError:
35 # 'agent-state' has been removed for new versions of Juju. Respond41 # 'agent-state' has been removed for new versions of Juju. Respond
36 # with the closest status parameter.42 # with the closest status parameter.
37 return entity['jujustatus']['Current']43 return entity['agent-status']['status']
3844
39 def _get_units_in_error(self, status=None):45 def _get_units_in_error(self, status=None):
40 units = []46 units = []
41 if status is None:47 if status is None:
42 status = self.status()48 status = self.status()
43 for s in status.get('services', {}).keys():49 for s in status.get('services', {}).keys():
44 for uid, u in status['services'][s].get('units', {}).items():50 for uid, u in (status['services'][s].get('units') or {}).items():
45 if 'error' in self._get_agent_state(u):51 if 'error' in self._get_agent_state(u):
46 units.append(uid)52 units.append(uid)
47 for uid, u in u.get('subordinates', {}).items():53 for uid, u in (u.get('subordinates') or {}).items():
48 if 'error' in self._get_agent_state(u):54 if 'error' in self._get_agent_state(u):
49 units.append(uid)55 units.append(uid)
50 return units56 return units
@@ -91,20 +97,20 @@
9197
92 params.extend([charm_url, name])98 params.extend([charm_url, name])
93 self._check_call(99 self._check_call(
94 params, self.log, "Error deploying service %r", name)100 params, self.log, "Error deploying application %r", name)
95101
96 def expose(self, name):102 def expose(self, name):
97 params = self._named_env(["juju", "expose", name])103 params = self._named_env(["juju", "expose", name])
98 self._check_call(104 self._check_call(
99 params, self.log, "Error exposing service %r", name)105 params, self.log, "Error exposing application %r", name)
100106
101 def terminate_machine(self, mid, wait=False, force=False):107 def terminate_machine(self, mid, wait=False, force=False):
102 """Terminate a machine.108 """Terminate a machine.
103109
104 Unless ``force=True``, the machine can't have any running units.110 Unless ``force=True``, the machine can't have any running units.
105 After removing the units or destroying the service, use wait_for_units111 After removing the units or destroying the application, use
106 to know when its safe to delete the machine (i.e., units have finished112 wait_for_units to know when its safe to delete the machine (i.e.,
107 executing stop hooks and are removed).113 units have finished executing stop hooks and are removed).
108114
109 """115 """
110 if ((isinstance(mid, int) and mid == 0) or116 if ((isinstance(mid, int) and mid == 0) or
@@ -130,21 +136,22 @@
130 def get_service_address(self, svc_name):136 def get_service_address(self, svc_name):
131 status = self.get_cli_status()137 status = self.get_cli_status()
132 if svc_name not in status['services']:138 if svc_name not in status['services']:
133 self.log.warning("Service %s does not exist", svc_name)139 self.log.warning("Application %s does not exist", svc_name)
134 return None140 return None
135 svc = status['services'][svc_name]141 svc = status['services'][svc_name]
136 if 'subordinate-to' in svc:142 if 'subordinate-to' in svc:
137 ps = svc['subordinate-to'][0]143 ps = svc['subordinate-to'][0]
138 self.log.info(144 self.log.info(
139 'Service %s is a subordinate to %s, finding principle service'145 'Application %s is a subordinate to %s, '
146 'finding principle application'
140 % (svc_name, ps))147 % (svc_name, ps))
141 return self.get_service_address(svc['subordinate-to'][0])148 return self.get_service_address(svc['subordinate-to'][0])
142149
143 units = svc.get('units', {})150 units = svc.get('units') or {}
144 unit_keys = list(sorted(units.keys()))151 unit_keys = list(sorted(units.keys()))
145 if unit_keys:152 if unit_keys:
146 return units[unit_keys[0]].get('public-address', '')153 return units[unit_keys[0]].get('public-address', '')
147 self.log.warning("Service %s has no units" % svc_name)154 self.log.warning("Application %s has no units" % svc_name)
148155
149 def get_cli_status(self):156 def get_cli_status(self):
150 params = self._named_env(["juju", "status", "--format=yaml"])157 params = self._named_env(["juju", "status", "--format=yaml"])
@@ -153,10 +160,21 @@
153 params, self.log, "Error getting status, is it bootstrapped?",160 params, self.log, "Error getting status, is it bootstrapped?",
154 stderr=fh)161 stderr=fh)
155 status = yaml_load(output)162 status = yaml_load(output)
156 return status163 return NormalizedStatus(status)
157164
158 def add_unit(self, service_name, machine_spec):165 def add_unit(self, service_name, machine_spec):
159 raise NotImplementedError()166 raise NotImplementedError()
160167
161 def set_annotation(self, entity, annotations, entity_type='service'):168 def set_annotation(self, entity, annotations, entity_type='service'):
162 raise NotImplementedError()169 raise NotImplementedError()
170
171
172class NormalizedStatus(AlternateKeyDict):
173 alternates = {
174 'services': ('Services', 'applications'),
175 'machines': ('Machines',),
176 'units': ('Units',),
177 'relations': ('Relations',),
178 'subordinates': ('Subordinates',),
179 'agent-state': ('AgentState',),
180 }
163181
=== modified file 'deployer/env/go.py'
--- deployer/env/go.py 2016-05-07 23:48:30 +0000
+++ deployer/env/go.py 2016-07-29 13:47:45 +0000
@@ -2,21 +2,25 @@
2from functools import cmp_to_key2from functools import cmp_to_key
3import time3import time
44
5from .base import BaseEnvironment5from .base import (
6 BaseEnvironment,
7 NormalizedStatus,
8)
9
6from ..utils import (10from ..utils import (
7 ErrorExit,11 ErrorExit,
8 get_juju_major_version,12 get_juju_major_version,
9 parse_constraints,13 parse_constraints,
10)14)
1115
12from jujuclient import (16from jujuclient.exc import (
13 EnvError,17 EnvError,
14 Environment as EnvironmentClient,
15 UnitErrors,18 UnitErrors,
16)19)
1720
18from .watchers import (21from .watchers import (
19 raise_on_errors,22 raise_on_errors,
23 NormalizedDelta,
20 WaitForMachineTermination,24 WaitForMachineTermination,
21 WaitForUnits,25 WaitForUnits,
22)26)
@@ -31,6 +35,14 @@
31 self.client = None35 self.client = None
32 self.juju_version = get_juju_major_version()36 self.juju_version = get_juju_major_version()
3337
38 @property
39 def client_class(self):
40 if self.juju_version == 1:
41 from jujuclient.juju1.environment import Environment
42 else:
43 from jujuclient.juju2.environment import Environment
44 return Environment
45
34 def add_machine(self, series="", constraints={}):46 def add_machine(self, series="", constraints={}):
35 """Add a top level machine to the Juju environment.47 """Add a top level machine to the Juju environment.
3648
@@ -41,9 +53,12 @@
41 constraints: a map of constraints (such as mem, arch, etc.) which53 constraints: a map of constraints (such as mem, arch, etc.) which
42 can be parsed by utils.parse_constraints54 can be parsed by utils.parse_constraints
43 """55 """
44 return self.client.add_machine(56 result = self.client.add_machine(
45 series=series,57 series=series,
46 constraints=parse_constraints(constraints))['Machine']58 constraints=parse_constraints(constraints))
59 if 'Machine' in result:
60 return result['Machine']
61 return result['machine']
4762
48 def add_unit(self, service_name, machine_spec):63 def add_unit(self, service_name, machine_spec):
49 return self.client.add_unit(service_name, machine_spec)64 return self.client.add_unit(service_name, machine_spec)
@@ -70,7 +85,7 @@
70 self.log, "Error getting env api endpoints, env bootstrapped?",85 self.log, "Error getting env api endpoints, env bootstrapped?",
71 stderr=fh)86 stderr=fh)
7287
73 self.client = EnvironmentClient.connect(self.name)88 self.client = self.client_class.connect(self.name)
74 self.log.debug("Connected to environment")89 self.log.debug("Connected to environment")
7590
76 def get_config(self, svc_name):91 def get_config(self, svc_name):
@@ -80,7 +95,7 @@
80 try:95 try:
81 return self.client.get_constraints(svc_name)96 return self.client.get_constraints(svc_name)
82 except EnvError as err:97 except EnvError as err:
83 if 'constraints do not apply to subordinate services' in str(err):98 if 'constraints do not apply to subordinate' in str(err):
84 return {}99 return {}
85 raise100 raise
86101
@@ -97,7 +112,7 @@
97 status = self.status()112 status = self.status()
98 destroyed = False113 destroyed = False
99 for s in status.get('services', {}).keys():114 for s in status.get('services', {}).keys():
100 self.log.debug(" Destroying service %s", s)115 self.log.debug(" Destroying application %s", s)
101 self.client.destroy_service(s)116 self.client.destroy_service(s)
102 destroyed = True117 destroyed = True
103118
@@ -137,7 +152,7 @@
137 containers = set()152 containers = set()
138153
139 def machine_sort(x, y):154 def machine_sort(x, y):
140 for ctype in ('lxc', 'kvm'):155 for ctype in ('lxc', 'lxd', 'kvm'):
141 for m in (x, y):156 for m in (x, y):
142 if ctype in m:157 if ctype in m:
143 container_hosts.add(m.split('/', 1)[0])158 container_hosts.add(m.split('/', 1)[0])
@@ -230,15 +245,16 @@
230 def set_annotation(self, entity_name, annotation, entity_type='service'):245 def set_annotation(self, entity_name, annotation, entity_type='service'):
231 """Set an annotation on an entity.246 """Set an annotation on an entity.
232247
233 entity_name: the name of the entity (machine, service, etc.) to248 entity_name: the name of the entity (machine, application, etc.) to
234 annotate.249 annotate.
235 annotation: a dict of key/value pairs to set on the entity.250 annotation: a dict of key/value pairs to set on the entity.
236 entity_type: the type of entity (machine, service, etc.) to annotate.251 entity_type: the type of entity (machine, application, etc.) to
252 annotate.
237 """253 """
238 return self.client.set_annotation(entity_name, entity_type, annotation)254 return self.client.set_annotation(entity_name, entity_type, annotation)
239255
240 def status(self):256 def status(self):
241 return self.client.get_stat()257 return NormalizedStatus(self.client.status())
242258
243 def log_errors(self, errors):259 def log_errors(self, errors):
244 """Log the given unit errors.260 """Log the given unit errors.
@@ -246,6 +262,14 @@
246 This can be used in the WaitForUnits error handling machinery, e.g.262 This can be used in the WaitForUnits error handling machinery, e.g.
247 see deployer.watchers.log_on_errors.263 see deployer.watchers.log_on_errors.
248 """264 """
265 for e in errors:
266 if 'StatusInfo' not in e:
267 # convert from juju2 format
268 e['Name'] = e['name']
269 e['MachineId'] = e['machine-id']
270 e['Status'] = e['workload-status']['current']
271 e['StatusInfo'] = e['workload-status']['message']
272
249 messages = [273 messages = [
250 'unit: {Name}: machine: {MachineId} agent-state: {Status} '274 'unit: {Name}: machine: {MachineId} agent-state: {Status} '
251 'details: {StatusInfo}'.format(**error) for error in errors275 'details: {StatusInfo}'.format(**error) for error in errors
@@ -266,8 +290,14 @@
266290
267 def _delta_event_log(self, et, ct, d):291 def _delta_event_log(self, et, ct, d):
268 # event type, change type, data292 # event type, change type, data
293 d = NormalizedDelta(d)
269 name = d.get('Name', d.get('Id', 'unknown'))294 name = d.get('Name', d.get('Id', 'unknown'))
270 state = d.get('Status', d.get('Life', 'unknown'))295 state = (
296 d.get('workload-status') or
297 d.get('Status') or
298 d.get('Life') or
299 'unknown'
300 )
271 if et == "relation":301 if et == "relation":
272 name = self._format_endpoints(d['Endpoints'])302 name = self._format_endpoints(d['Endpoints'])
273 state = "created"303 state = "created"
274304
=== modified file 'deployer/env/gui.py'
--- deployer/env/gui.py 2016-05-03 16:03:18 +0000
+++ deployer/env/gui.py 2016-07-29 13:47:45 +0000
@@ -4,7 +4,7 @@
4See <https://code.launchpad.net/~juju-gui/charms/precise/juju-gui/trunk>.4See <https://code.launchpad.net/~juju-gui/charms/precise/juju-gui/trunk>.
5"""5"""
66
7from .go import GoEnvironment, EnvironmentClient7from .go import GoEnvironment
8from ..utils import get_qualified_charm_url, parse_constraints8from ..utils import get_qualified_charm_url, parse_constraints
99
1010
@@ -27,7 +27,7 @@
27 client is already connected.27 client is already connected.
28 """28 """
29 if self.client is None:29 if self.client is None:
30 self.client = EnvironmentClient(self.api_endpoint)30 self.client = self.client_class(self.api_endpoint)
31 self.client.login(self._password, user=self._username)31 self.client.login(self._password, user=self._username)
3232
33 def close(self):33 def close(self):
3434
=== modified file 'deployer/env/mem.py'
--- deployer/env/mem.py 2016-05-07 23:48:30 +0000
+++ deployer/env/mem.py 2016-07-29 13:47:45 +0000
@@ -1,7 +1,9 @@
1from __future__ import absolute_import1from __future__ import absolute_import
2from deployer.utils import parse_constraints2from deployer.utils import parse_constraints
3from jujuclient import (UnitErrors,3from jujuclient.exc import (
4 EnvError)4 UnitErrors,
5 EnvError,
6)
5from six.moves import range7from six.moves import range
68
79
810
=== modified file 'deployer/env/watchers.py'
--- deployer/env/watchers.py 2016-05-07 23:48:30 +0000
+++ deployer/env/watchers.py 2016-07-29 13:47:45 +0000
@@ -1,15 +1,34 @@
1"""A collection of juju-core environment watchers."""1"""A collection of juju-core environment watchers."""
22
3from __future__ import absolute_import3from __future__ import absolute_import
4from jujuclient import WatchWrapper4from jujuclient.watch import WatchWrapper
55
6from ..utils import ErrorExit6from ..utils import (
7 AlternateKeyDict,
8 ErrorExit,
9)
710
8# _status_map provides a translation of Juju 2 status codes to the closest11# _status_map provides a translation of Juju 2 status codes to the closest
9# Juju 1 equivalent. Only defines codes that need translation.12# Juju 1 equivalent. Only defines codes that need translation.
10_status_map = {'idle': 'started'}13_status_map = {'idle': 'started'}
1114
1215
16class NormalizedDelta(AlternateKeyDict):
17 alternates = {
18 'Name': ('name',),
19 'Id': ('id',),
20 'Status': ('status',),
21 'JujuStatus': ('juju-status',),
22 'Current': ('current',),
23 'Life': ('life',),
24 'Endpoints': ('endpoints',),
25 'Service': ('service', 'application'),
26 'ServiceName': ('application-name',),
27 'Relation': ('relation',),
28 'Role': ('role',),
29 }
30
31
13class WaitForMachineTermination(WatchWrapper):32class WaitForMachineTermination(WatchWrapper):
14 """Wait until the given machines are terminated."""33 """Wait until the given machines are terminated."""
1534
@@ -21,6 +40,8 @@
21 def process(self, entity_type, change, data):40 def process(self, entity_type, change, data):
22 if entity_type != 'machine':41 if entity_type != 'machine':
23 return42 return
43
44 data = NormalizedDelta(data)
24 if change == 'remove' and data['Id'] in self.machines:45 if change == 'remove' and data['Id'] in self.machines:
25 self.machines.remove(data['Id'])46 self.machines.remove(data['Id'])
26 else:47 else:
@@ -57,6 +78,8 @@
57 def process(self, entity, action, data):78 def process(self, entity, action, data):
58 if entity != 'unit':79 if entity != 'unit':
59 return80 return
81
82 data = NormalizedDelta(data)
60 if (self.services is None) or (data['Service'] in self.services):83 if (self.services is None) or (data['Service'] in self.services):
61 unit_name = data['Name']84 unit_name = data['Name']
62 if action == 'remove' and unit_name in self.units:85 if action == 'remove' and unit_name in self.units:
@@ -72,19 +95,21 @@
72 units_in_error = self.units_in_error95 units_in_error = self.units_in_error
73 for unit_name, data in self.units.items():96 for unit_name, data in self.units.items():
74 try:97 try:
75 status = data['Status']98 err_status = data['Status']
99 goal_status = err_status
76 except KeyError:100 except KeyError:
77 # 'Status' has been removed from newer versions of Juju.101 # 'Status' has been removed from newer versions of Juju.
78 # Respond with the closest status parameter, translating it102 # Respond with the closest status parameter, translating it
79 # through the _status_map. If the status value is not in103 # through the _status_map. If the status value is not in
80 # _status_map, just use the original value.104 # _status_map, just use the original value.
81 status = data['JujuStatus']['Current']105 err_status = data['workload-status']['current']
82 status = _status_map.get(status, status)106 goal_status = data['agent-status']['current']
83 if status == 'error':107 goal_status = _status_map.get(goal_status, goal_status)
108 if err_status == 'error':
84 if unit_name not in units_in_error:109 if unit_name not in units_in_error:
85 units_in_error.append(unit_name)110 units_in_error.append(unit_name)
86 new_errors.append(data)111 new_errors.append(data)
87 elif status != goal_state:112 elif goal_status != goal_state:
88 ready = False113 ready = False
89 if new_errors and goal_state != 'removed' and callable(on_errors):114 if new_errors and goal_state != 'removed' and callable(on_errors):
90 on_errors(new_errors)115 on_errors(new_errors)
91116
=== modified file 'deployer/errors.py'
--- deployer/errors.py 2016-05-07 23:48:30 +0000
+++ deployer/errors.py 2016-07-29 13:47:45 +0000
@@ -1,4 +1,4 @@
1# TODO make deployer specific exceptions,1# TODO make deployer specific exceptions,
2# also move errorexit from utils to here.2# also move errorexit from utils to here.
3from __future__ import absolute_import3from __future__ import absolute_import
4from jujuclient import UnitErrors, EnvError # noqa4from jujuclient.exc import UnitErrors, EnvError # noqa
55
=== modified file 'deployer/service.py'
--- deployer/service.py 2016-05-12 20:22:06 +0000
+++ deployer/service.py 2016-07-29 13:47:45 +0000
@@ -2,8 +2,25 @@
2import itertools2import itertools
33
4from .feedback import Feedback4from .feedback import Feedback
5from .utils import get_juju_major_version
5from six.moves import map6from six.moves import map
67
8# Map of container-type-used-in-bundle to actual-container-type-passed-to-juju.
9if get_juju_major_version() == 1:
10 # lxd not supported on juju1
11 CONTAINER_TYPES = {
12 'lxc': 'lxc',
13 'kvm': 'kvm',
14 }
15else:
16 # If you use 'lxc' in a bundle with juju2, deployer will translate it to
17 # 'lxd' before passing the deploy command to juju.
18 CONTAINER_TYPES = {
19 'lxc': 'lxd',
20 'lxd': 'lxd',
21 'kvm': 'kvm',
22 }
23
724
8class Service(object):25class Service(object):
926
@@ -82,8 +99,8 @@
82 # Should be caught in validate relations but sanity check99 # Should be caught in validate relations but sanity check
83 # for concurrency.100 # for concurrency.
84 self.deployment.log.error(101 self.deployment.log.error(
85 "Service %s to be deployed with non-existent service %s",102 "Application %s to be deployed with non-existent "
86 svc.name, placement)103 "application %s", svc.name, placement)
87 # Prefer continuing deployment with a new machine rather104 # Prefer continuing deployment with a new machine rather
88 # than an in-progress abort.105 # than an in-progress abort.
89 return None106 return None
@@ -91,7 +108,8 @@
91 svc_units = with_service['units']108 svc_units = with_service['units']
92 if int(u_idx) >= len(svc_units):109 if int(u_idx) >= len(svc_units):
93 self.deployment.log.warning(110 self.deployment.log.warning(
94 "Service:%s, Deploy-with-service:%s, Requested-unit-index=%s, "111 "Application:%s, Deploy-with-application:%s, "
112 "Requested-unit-index=%s, "
95 "Cannot solve, falling back to default placement",113 "Cannot solve, falling back to default placement",
96 svc.name, placement, u_idx)114 svc.name, placement, u_idx)
97 return None115 return None
@@ -100,7 +118,7 @@
100 machine = svc_units[unit_names[int(u_idx)]].get('machine')118 machine = svc_units[unit_names[int(u_idx)]].get('machine')
101 if not machine:119 if not machine:
102 self.deployment.log.warning(120 self.deployment.log.warning(
103 "Service:%s deploy-with unit missing machine %s",121 "Application:%s deploy-with unit missing machine %s",
104 svc.name, unit_names[int(u_idx)])122 svc.name, unit_names[int(u_idx)])
105 return None123 return None
106 return self._format_placement(machine, container)124 return self._format_placement(machine, container)
@@ -134,23 +152,23 @@
134152
135 for idx, p in enumerate(unit_placement):153 for idx, p in enumerate(unit_placement):
136 container, p, u_idx = self._parse_placement(p)154 container, p, u_idx = self._parse_placement(p)
137 if container and container not in ('lxc', 'kvm'):155 if container and container not in CONTAINER_TYPES:
138 feedback.error(156 feedback.error(
139 "Invalid container type:%s service: %s placement: %s"157 "Invalid container type:%s application: %s placement: %s"
140 % (container, self.service.name, unit_placement[idx]))158 % (container, self.service.name, unit_placement[idx]))
141 if u_idx:159 if u_idx:
142 if p in ('maas', 'zone'):160 if p in ('maas', 'zone'):
143 continue161 continue
144 if not u_idx.isdigit():162 if not u_idx.isdigit():
145 feedback.error(163 feedback.error(
146 "Invalid service:%s placement: %s" % (164 "Invalid application:%s placement: %s" % (
147 self.service.name, unit_placement[idx]))165 self.service.name, unit_placement[idx]))
148 if p.isdigit():166 if p.isdigit():
149 if p == '0' or p in machines or self.arbitrary_machines:167 if p == '0' or p in machines or self.arbitrary_machines:
150 continue168 continue
151 else:169 else:
152 feedback.error(170 feedback.error(
153 ("Service placement to machine "171 ("Application placement to machine "
154 "not supported %s to %s") % (172 "not supported %s to %s") % (
155 self.service.name, unit_placement[idx]))173 self.service.name, unit_placement[idx]))
156 elif p in services:174 elif p in services:
@@ -160,11 +178,11 @@
160 self.service.name, p, services[p].unit_placement))178 self.service.name, p, services[p].unit_placement))
161 elif self.deployment.get_charm_for(p).is_subordinate():179 elif self.deployment.get_charm_for(p).is_subordinate():
162 feedback.error(180 feedback.error(
163 "Cannot place to a subordinate service: %s -> %s" % (181 "Cannot place to a subordinate application: "
164 self.service.name, p))182 "%s -> %s" % (self.service.name, p))
165 else:183 else:
166 feedback.error(184 feedback.error(
167 "Invalid service placement %s to %s" % (185 "Invalid application placement %s to %s" % (
168 self.service.name, unit_placement[idx]))186 self.service.name, unit_placement[idx]))
169 return feedback187 return feedback
170188
@@ -188,6 +206,7 @@
188206
189 if ':' in unit_placement:207 if ':' in unit_placement:
190 container, placement = unit_placement.split(":")208 container, placement = unit_placement.split(":")
209 container = CONTAINER_TYPES[container]
191 if '=' in placement:210 if '=' in placement:
192 placement, u_idx = placement.split("=")211 placement, u_idx = placement.split("=")
193212
@@ -222,14 +241,15 @@
222 """Fill the placement spec with necessary data.241 """Fill the placement spec with necessary data.
223242
224 From the spec:243 From the spec:
225 A unit placement may be specified with a service name only, in which244 A unit placement may be specified with an application name only,
226 case its unit number is assumed to be one more than the unit number of245 in which case its unit number is assumed to be one more than the
227 the previous unit in the list with the same service, or zero if there246 unit number of the previous unit in the list with the same
228 were none.247 application, or zero if there were none.
229248
230 If there are less elements in To than NumUnits, the last element is249 If there are less elements in To than NumUnits, the last element is
231 replicated to fill it. If there are no elements (or To is omitted),250 replicated to fill it. If there are no elements (or To is omitted),
232 "new" is replicated.251 "new" is replicated.
252
233 """253 """
234 unit_mapping = self.service.unit_placement254 unit_mapping = self.service.unit_placement
235 unit_count = self.service.num_units255 unit_count = self.service.num_units
@@ -264,24 +284,27 @@
264284
265 This splits the placement into a container, a placement, and a unit285 This splits the placement into a container, a placement, and a unit
266 number. Both container and unit number are optional and can be None.286 number. Both container and unit number are optional and can be None.
287
267 """288 """
268 container = unit_number = None289 container = unit_number = None
269 if ':' in placement:290 if ':' in placement:
270 container, placement = placement.split(':')291 container, placement = placement.split(':')
292 container = CONTAINER_TYPES.get(container, container)
271 if '/' in placement:293 if '/' in placement:
272 placement, unit_number = placement.split('/')294 placement, unit_number = placement.split('/')
273 return container, placement, unit_number295 return container, placement, unit_number
274296
275 def validate(self):297 def validate(self):
276 """Validate the placement of a service and all of its units.298 """Validate the placement of an application and all of its units.
277299
278 If a service has a 'to' block specified, the list of machines, units,300 If an application has a 'to' block specified, the list of machines,
279 containers, and/or services must be internally consistent, consistent301 units, containers, and/or applications must be internally consistent,
280 with other services in the deployment, and consistent with any machines302 consistent with other applications in the deployment, and consistent
281 specified in the 'machines' block of the deployment.303 with any machines specified in the 'machines' block of the deployment.
282304
283 A feedback object is returned, potentially with errors and warnings305 A feedback object is returned, potentially with errors and warnings
284 inside it.306 inside it.
307
285 """308 """
286 feedback = Feedback()309 feedback = Feedback()
287310
@@ -301,9 +324,9 @@
301 container, target, unit_number = self._parse_placement(placement)324 container, target, unit_number = self._parse_placement(placement)
302325
303 # Validate the container type.326 # Validate the container type.
304 if container and container not in ('lxc', 'kvm'):327 if container and container not in CONTAINER_TYPES:
305 feedback.error(328 feedback.error(
306 'Invalid container type: {} service: {} placement: {}'329 'Invalid container type: {} application: {} placement: {}'
307 ''.format(container, service_name, placement))330 ''.format(container, service_name, placement))
308 # Specify an existing machine (or, if the number is in the331 # Specify an existing machine (or, if the number is in the
309 # list of machine specs, one of those).332 # list of machine specs, one of those).
@@ -311,7 +334,7 @@
311 continue334 continue
312 if target.isdigit():335 if target.isdigit():
313 feedback.error(336 feedback.error(
314 'Service placement to machine not supported: {} to {}'337 'Application placement to machine not supported: {} to {}'
315 ''.format(service_name, placement))338 ''.format(service_name, placement))
316 # Specify a service for co-location.339 # Specify a service for co-location.
317 elif target in services:340 elif target in services:
@@ -326,24 +349,24 @@
326 continue349 continue
327 if unit_number > services[target].num_units:350 if unit_number > services[target].num_units:
328 feedback.error(351 feedback.error(
329 'Service unit does not exist: {} to {}/{}'352 'Application unit does not exist: {} to {}/{}'
330 ''.format(service_name, target, unit_number))353 ''.format(service_name, target, unit_number))
331 continue354 continue
332 if self.deployment.get_charm_for(target).is_subordinate():355 if self.deployment.get_charm_for(target).is_subordinate():
333 feedback.error(356 feedback.error(
334 'Cannot place to a subordinate service: {} -> {}'357 'Cannot place to a subordinate application: {} -> {}'
335 ''.format(service_name, target))358 ''.format(service_name, target))
336 # Create a new machine or container.359 # Create a new machine or container.
337 elif target == 'new':360 elif target == 'new':
338 continue361 continue
339 else:362 else:
340 feedback.error(363 feedback.error(
341 'Invalid service placement: {} to {}'364 'Invalid application placement: {} to {}'
342 ''.format(service_name, placement))365 ''.format(service_name, placement))
343 return feedback366 return feedback
344367
345 def get_new_machines_for_containers(self):368 def get_new_machines_for_containers(self):
346 """Return a list of containers in the service's unit placement that369 """Return a list of containers in the application's unit placement that
347 have been requested to be put on new machines."""370 have been requested to be put on new machines."""
348 new_machines = []371 new_machines = []
349 unit = itertools.count()372 unit = itertools.count()
350373
=== modified file 'deployer/tests/test_deployment.py'
--- deployer/tests/test_deployment.py 2016-05-08 01:11:12 +0000
+++ deployer/tests/test_deployment.py 2016-07-29 13:47:45 +0000
@@ -6,6 +6,7 @@
66
7from deployer.deployment import Deployment7from deployer.deployment import Deployment
8from deployer.utils import setup_logging, ErrorExit8from deployer.utils import setup_logging, ErrorExit
9from deployer.service import CONTAINER_TYPES
910
10from .base import Base, skip_if_offline11from .base import Base, skip_if_offline
1112
@@ -151,18 +152,18 @@
151152
152 self.assertEqual(p.colocate(status, 'asdf', '1', '', svc),153 self.assertEqual(p.colocate(status, 'asdf', '1', '', svc),
153 None)154 None)
154 self.assertIn('Service bar to be deployed with non-existent service '155 self.assertIn('Application bar to be deployed with non-existent '
155 'asdf',156 'application asdf',
156 self.output.getvalue())157 self.output.getvalue())
157 self.assertEqual(p.colocate(status, 'foo', '2', '', svc),158 self.assertEqual(p.colocate(status, 'foo', '2', '', svc),
158 None)159 None)
159 self.assertIn('Service:bar, Deploy-with-service:foo, '160 self.assertIn('Application:bar, Deploy-with-application:foo, '
160 'Requested-unit-index=2, Cannot solve, '161 'Requested-unit-index=2, Cannot solve, '
161 'falling back to default placement',162 'falling back to default placement',
162 self.output.getvalue())163 self.output.getvalue())
163 self.assertEqual(p.colocate(status, 'foo', '1', '', svc),164 self.assertEqual(p.colocate(status, 'foo', '1', '', svc),
164 None)165 None)
165 self.assertIn('Service:bar deploy-with unit missing machine 2',166 self.assertIn('Application:bar deploy-with unit missing machine 2',
166 self.output.getvalue())167 self.output.getvalue())
167 self.assertEqual(p.colocate(status, 'foo', '0', '', svc), 1)168 self.assertEqual(p.colocate(status, 'foo', '0', '', svc), 1)
168169
@@ -201,9 +202,11 @@
201 deployment.validate_placement()202 deployment.validate_placement()
202 output = self.output.getvalue()203 output = self.output.getvalue()
203 self.assertIn(204 self.assertIn(
204 'Cannot place to a subordinate service: ceph -> nrpe\n', output)205 'Cannot place to a subordinate application: '
206 'ceph -> nrpe\n', output)
205 self.assertIn(207 self.assertIn(
206 'Cannot place to a subordinate service: nova-compute -> nrpe\n',208 'Cannot place to a subordinate application: '
209 'nova-compute -> nrpe\n',
207 output)210 output)
208211
209 @skip_if_offline212 @skip_if_offline
@@ -215,7 +218,8 @@
215 deployment.validate_placement()218 deployment.validate_placement()
216 output = self.output.getvalue()219 output = self.output.getvalue()
217 self.assertIn(220 self.assertIn(
218 'Cannot place to a subordinate service: nova-compute -> nrpe\n',221 'Cannot place to a subordinate application: '
222 'nova-compute -> nrpe\n',
219 output)223 output)
220224
221 def test_validate_invalid_unit_number(self):225 def test_validate_invalid_unit_number(self):
@@ -228,6 +232,12 @@
228 'Invalid unit number for placement: django to bad-wolf\n', output)232 'Invalid unit number for placement: django to bad-wolf\n', output)
229233
230 def test_get_unit_placement_v3(self):234 def test_get_unit_placement_v3(self):
235 def _container(s):
236 if ':' in s:
237 typ, num = s.split(':')
238 return '{}:{}'.format(CONTAINER_TYPES[typ], num)
239 return CONTAINER_TYPES[typ]
240
231 d = self.get_named_deployment_v3("stack-placement.yaml", "stack")241 d = self.get_named_deployment_v3("stack-placement.yaml", "stack")
232 status = {242 status = {
233 'services': {243 'services': {
@@ -242,12 +252,12 @@
242 self.assertEqual(placement.get(2), None)252 self.assertEqual(placement.get(2), None)
243253
244 placement = d.get_unit_placement('quantum', status)254 placement = d.get_unit_placement('quantum', status)
245 self.assertEqual(placement.get(0), 'lxc:1')255 self.assertEqual(placement.get(0), _container('lxc:1'))
246 self.assertEqual(placement.get(2), 'lxc:3')256 self.assertEqual(placement.get(2), _container('lxc:3'))
247 self.assertEqual(placement.get(3), None)257 self.assertEqual(placement.get(3), None)
248258
249 placement = d.get_unit_placement('verity', status)259 placement = d.get_unit_placement('verity', status)
250 self.assertEqual(placement.get(0), 'lxc:3')260 self.assertEqual(placement.get(0), _container('lxc:3'))
251261
252 placement = d.get_unit_placement('mysql', status)262 placement = d.get_unit_placement('mysql', status)
253 self.assertEqual(placement.get(0), '0')263 self.assertEqual(placement.get(0), '0')
@@ -256,11 +266,11 @@
256 self.assertEqual(placement.get(0), '3')266 self.assertEqual(placement.get(0), '3')
257267
258 placement = d.get_unit_placement('lxc-service', status)268 placement = d.get_unit_placement('lxc-service', status)
259 self.assertEqual(placement.get(0), 'lxc:2')269 self.assertEqual(placement.get(0), _container('lxc:2'))
260 self.assertEqual(placement.get(1), 'lxc:3')270 self.assertEqual(placement.get(1), _container('lxc:3'))
261 self.assertEqual(placement.get(2), 'lxc:1')271 self.assertEqual(placement.get(2), _container('lxc:1'))
262 self.assertEqual(placement.get(3), 'lxc:1')272 self.assertEqual(placement.get(3), _container('lxc:1'))
263 self.assertEqual(placement.get(4), 'lxc:3')273 self.assertEqual(placement.get(4), _container('lxc:3'))
264274
265 def test_fill_placement_v4(self):275 def test_fill_placement_v4(self):
266 d = self.get_deployment_v4('fill_placement.yaml')276 d = self.get_deployment_v4('fill_placement.yaml')
@@ -290,7 +300,7 @@
290 self.assertEqual(u, '1')300 self.assertEqual(u, '1')
291301
292 c, p, u = placement._parse_placement('lxc:mysql')302 c, p, u = placement._parse_placement('lxc:mysql')
293 self.assertEqual(c, 'lxc')303 self.assertEqual(c, CONTAINER_TYPES['lxc'])
294 self.assertEqual(p, 'mysql')304 self.assertEqual(p, 'mysql')
295 self.assertEqual(u, None)305 self.assertEqual(u, None)
296306
@@ -299,12 +309,13 @@
299 placement = d.get_unit_placement('mysql', {})309 placement = d.get_unit_placement('mysql', {})
300 feedback = placement.validate()310 feedback = placement.validate()
301 self.assertEqual(feedback.get_errors(), [311 self.assertEqual(feedback.get_errors(), [
302 'Invalid container type: asdf service: mysql placement: asdf:0',312 'Invalid container type: asdf application: mysql '
303 'Service placement to machine not supported: mysql to asdf:0',313 'placement: asdf:0',
304 'Invalid service placement: mysql to lxc:asdf',314 'Application placement to machine not supported: mysql to asdf:0',
305 'Service placement to machine not supported: mysql to 1',315 'Invalid application placement: mysql to lxc:asdf',
306 'Service unit does not exist: mysql to wordpress/3',316 'Application placement to machine not supported: mysql to 1',
307 'Invalid service placement: mysql to asdf'])317 'Application unit does not exist: mysql to wordpress/3',
318 'Invalid application placement: mysql to asdf'])
308319
309 def test_get_unit_placement_v4_simple(self):320 def test_get_unit_placement_v4_simple(self):
310 d = self.get_deployment_v4('simple.yaml')321 d = self.get_deployment_v4('simple.yaml')
@@ -419,7 +430,10 @@
419 d.set_machines(machines)430 d.set_machines(machines)
420431
421 placement = d.get_unit_placement('mysql', status)432 placement = d.get_unit_placement('mysql', status)
422 self.assertEqual(placement.get(0), 'lxc:1')433 self.assertEqual(
434 placement.get(0),
435 '{}:1'.format(CONTAINER_TYPES['lxc'])
436 )
423437
424 placement = d.get_unit_placement('mediawiki', status)438 placement = d.get_unit_placement('mediawiki', status)
425 self.assertEqual(placement.get(0), 1)439 self.assertEqual(placement.get(0), 1)
@@ -446,7 +460,10 @@
446 self.assertEqual(460 self.assertEqual(
447 placement.get_new_machines_for_containers(),461 placement.get_new_machines_for_containers(),
448 ['mysql/0'])462 ['mysql/0'])
449 self.assertEqual(placement.get(0), 'lxc:2')463 self.assertEqual(
464 placement.get(0),
465 '{}:2'.format(CONTAINER_TYPES['lxc'])
466 )
450467
451 placement = d.get_unit_placement('mediawiki', status)468 placement = d.get_unit_placement('mediawiki', status)
452 self.assertEqual(placement.get(0), 1)469 self.assertEqual(placement.get(0), 1)
453470
=== modified file 'deployer/tests/test_goenv.py'
--- deployer/tests/test_goenv.py 2016-05-07 23:02:56 +0000
+++ deployer/tests/test_goenv.py 2016-07-29 13:47:45 +0000
@@ -77,7 +77,7 @@
77 self.assertIn(u['agent-state'],77 self.assertIn(u['agent-state'],
78 ("allocating", "pending", "started"))78 ("allocating", "pending", "started"))
79 except KeyError:79 except KeyError:
80 self.assertIn(u['jujustatus']['Current'],80 self.assertIn(u['agent-status']['status'],
81 ("allocating", "idle"))81 ("allocating", "idle"))
8282
83 def test_add_machine(self):83 def test_add_machine(self):
8484
=== modified file 'deployer/tests/test_guienv.py'
--- deployer/tests/test_guienv.py 2016-05-07 23:02:56 +0000
+++ deployer/tests/test_guienv.py 2016-07-29 13:47:45 +0000
@@ -9,7 +9,7 @@
9)9)
1010
1111
12@mock.patch('deployer.env.gui.EnvironmentClient')12@mock.patch.object(GUIEnvironment, 'client_class')
13class TestGUIEnvironment(unittest.TestCase):13class TestGUIEnvironment(unittest.TestCase):
1414
15 endpoint = 'wss://api.example.com:17070'15 endpoint = 'wss://api.example.com:17070'
1616
=== modified file 'deployer/tests/test_guiserver.py'
--- deployer/tests/test_guiserver.py 2016-05-07 23:02:56 +0000
+++ deployer/tests/test_guiserver.py 2016-07-29 13:47:45 +0000
@@ -28,8 +28,8 @@
28 # When adding/modifying options, ensure the defaults are sane for us.28 # When adding/modifying options, ensure the defaults are sane for us.
29 expected_keys = set([29 expected_keys = set([
30 'bootstrap', 'branch_only', 'configs', 'debug', 'deploy_delay',30 'bootstrap', 'branch_only', 'configs', 'debug', 'deploy_delay',
31 'deployment', 'description', 'destroy_services', 'diff',31 'deployment', 'description', 'destroy_applications', 'diff',
32 'find_service', 'ignore_errors', 'juju_env', 'list_deploys',32 'find_application', 'ignore_errors', 'juju_env', 'list_deploys',
33 'no_local_mods', 'no_relations', 'overrides', 'rel_wait',33 'no_local_mods', 'no_relations', 'overrides', 'rel_wait',
34 'retry_count', 'series', 'skip_unit_wait', 'terminate_machines',34 'retry_count', 'series', 'skip_unit_wait', 'terminate_machines',
35 'timeout', 'update_charms', 'verbose', 'watch'35 'timeout', 'update_charms', 'verbose', 'watch'
@@ -46,9 +46,9 @@
46 self.assertFalse(options.debug)46 self.assertFalse(options.debug)
47 self.assertEqual(0, options.deploy_delay)47 self.assertEqual(0, options.deploy_delay)
48 self.assertIsNone(options.deployment)48 self.assertIsNone(options.deployment)
49 self.assertFalse(options.destroy_services)49 self.assertFalse(options.destroy_applications)
50 self.assertFalse(options.diff)50 self.assertFalse(options.diff)
51 self.assertIsNone(options.find_service)51 self.assertIsNone(options.find_application)
52 self.assertTrue(options.ignore_errors)52 self.assertTrue(options.ignore_errors)
53 self.assertEqual(os.getenv("JUJU_ENV"), options.juju_env)53 self.assertEqual(os.getenv("JUJU_ENV"), options.juju_env)
54 self.assertFalse(options.list_deploys)54 self.assertFalse(options.list_deploys)
5555
=== modified file 'deployer/utils.py'
--- deployer/utils.py 2016-05-13 14:16:48 +0000
+++ deployer/utils.py 2016-07-29 13:47:45 +0000
@@ -427,3 +427,40 @@
427 if x.name == placement:427 if x.name == placement:
428 return True428 return True
429 return False429 return False
430
431
432class AlternateKeyDict(dict):
433 def __getitem__(self, key):
434 try:
435 val = super(AlternateKeyDict, self).__getitem__(key)
436 if isinstance(val, dict):
437 return self.__class__(val)
438 return val
439 except KeyError:
440 if key not in self.alternates:
441 raise
442
443 for alt in self.alternates[key]:
444 if alt in self:
445 return self[alt]
446 raise
447
448 def get(self, key, default=None):
449 try:
450 return self.__getitem__(key)
451 except KeyError:
452 return default
453
454 def values(self):
455 for val in super(AlternateKeyDict, self).values():
456 if isinstance(val, dict):
457 yield self.__class__(val)
458 else:
459 yield val
460
461 def items(self):
462 for key, val in super(AlternateKeyDict, self).items():
463 if isinstance(val, dict):
464 yield key, self.__class__(val)
465 else:
466 yield key, val
430467
=== modified file 'tox.ini'
--- tox.ini 2016-05-13 12:26:48 +0000
+++ tox.ini 2016-07-29 13:47:45 +0000
@@ -15,7 +15,7 @@
15 JUJU_DATA = {homedir}/.local/share/juju15 JUJU_DATA = {homedir}/.local/share/juju
16 HOME = {env:HOME}16 HOME = {env:HOME}
17commands=17commands=
18 nosetests --with-coverage --cover-package=deployer deployer/tests18 nosetests -s --with-coverage --cover-package=deployer deployer/tests
1919
20[testenv:pep8]20[testenv:pep8]
21commands = flake8 deployer21commands = flake8 deployer

Subscribers

People subscribed via source and target branches