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