Merge lp:~frankban/juju-deployer/unit-errors into lp:juju-deployer
- unit-errors
- Merge into trunk
Status: | Merged | ||||||||
---|---|---|---|---|---|---|---|---|---|
Merged at revision: | 97 | ||||||||
Proposed branch: | lp:~frankban/juju-deployer/unit-errors | ||||||||
Merge into: | lp:juju-deployer | ||||||||
Diff against target: |
903 lines (+462/-170) 12 files modified
deployer/action/importer.py (+23/-16) deployer/cli.py (+4/-0) deployer/deployment.py (+4/-0) deployer/env/go.py (+44/-56) deployer/env/mem.py (+1/-2) deployer/env/py.py (+20/-14) deployer/env/watchers.py (+110/-0) deployer/guiserver.py (+8/-0) deployer/tests/test_deployment.py (+8/-0) deployer/tests/test_guiserver.py (+48/-32) deployer/tests/test_pyenv.py (+46/-50) deployer/tests/test_watchers.py (+146/-0) |
||||||||
To merge this branch: | bzr merge lp:~frankban/juju-deployer/unit-errors | ||||||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Brad Crittenden (community) | code | Approve | |
juju-deployers | Pending | ||
Review via email: mp+206967@code.launchpad.net |
Commit message
Proceed with bundle deployments on unit errors.
Description of the change
Proceed with bundle deployments on unit errors.
This branch introduces a new watcher implementation
which allows for 1) watching the deployment process for
a subset of all the environment units, e.g. those
belonging to the bundle services and 2) react to unit
errors in different ways (e.g., a flag can be used to
make the importer proceed even if there are errors).
The current default behavior is maintained. The guiserver
code path instead configures the importer to complete the
deployment even if units are in an error state.
- 97. By Francesco Banconi
-
Changes as per review.
Francesco Banconi (frankban) wrote : | # |
> Might be nice to mention in the help for '--ignore-errors' the relationship to
> retry_count.
Done.
> Otherwise this branch looks really good. I'm -0 on changes like 'options =
> self.options' in existing code due to the noise for no real benefit, but "meh"
> as you say. :)
Yeah, those kind of changes a far more readable e.g. in rietveld, where word-based diff helps reducing the noise.
Thanks for the review Brad.
Kapil Thangavelu (hazmat) wrote : | # |
the watchers.
- 98. By Francesco Banconi
-
Merge trunk and resolve conflicts.
- 99. By Francesco Banconi
-
Fix tests.
Preview Diff
1 | === modified file 'deployer/action/importer.py' |
2 | --- deployer/action/importer.py 2014-01-23 14:14:28 +0000 |
3 | +++ deployer/action/importer.py 2014-02-20 12:33:52 +0000 |
4 | @@ -2,6 +2,7 @@ |
5 | import time |
6 | |
7 | from .base import BaseAction |
8 | +from ..env import watchers |
9 | from ..errors import UnitErrors |
10 | from ..utils import ErrorExit |
11 | |
12 | @@ -158,27 +159,32 @@ |
13 | return True |
14 | return False |
15 | |
16 | - def wait_for_units(self, ignore_error=False): |
17 | + def wait_for_units(self, ignore_errors=False): |
18 | timeout = self.options.timeout - (time.time() - self.start_time) |
19 | if timeout < 0: |
20 | self.log.error("Reached deployment timeout.. exiting") |
21 | raise ErrorExit() |
22 | - try: |
23 | - self.env.wait_for_units( |
24 | - int(timeout), watch=self.options.watch, no_exit=ignore_error) |
25 | - except UnitErrors: |
26 | - if not ignore_error: |
27 | - raise |
28 | + # Set up the callback to be called in case of unit errors: if |
29 | + # ignore_errors is True errors are just logged, otherwise we exit the |
30 | + # program. |
31 | + if ignore_errors: |
32 | + on_errors = watchers.log_on_errors(self.env) |
33 | + else: |
34 | + on_errors = watchers.exit_on_errors(self.env) |
35 | + self.env.wait_for_units( |
36 | + int(timeout), watch=self.options.watch, |
37 | + services=self.deployment.get_service_names(), on_errors=on_errors) |
38 | |
39 | def run(self): |
40 | + options = self.options |
41 | self.start_time = time.time() |
42 | - if self.options.bootstrap: |
43 | + if options.bootstrap: |
44 | self.env.bootstrap() |
45 | self.env.connect() |
46 | |
47 | # Get charms |
48 | self.get_charms() |
49 | - if self.options.branch_only: |
50 | + if options.branch_only: |
51 | return |
52 | |
53 | self.deploy_services() |
54 | @@ -189,20 +195,21 @@ |
55 | time.sleep(5.1) |
56 | self.add_units() |
57 | |
58 | + ignore_errors = bool(options.retry_count) or options.ignore_errors |
59 | self.log.debug("Waiting for units before adding relations") |
60 | - self.wait_for_units() |
61 | + self.wait_for_units(ignore_errors=ignore_errors) |
62 | |
63 | rels_created = self.add_relations() |
64 | |
65 | # Wait for the units to be up before waiting for rel stability. |
66 | if rels_created: |
67 | self.log.debug( |
68 | - "Waiting for relation convergence %ds", self.options.rel_wait) |
69 | - time.sleep(self.options.rel_wait) |
70 | - self.wait_for_units(self.options.retry_count) |
71 | + "Waiting for relation convergence %ds", options.rel_wait) |
72 | + time.sleep(options.rel_wait) |
73 | + self.wait_for_units(ignore_errors=ignore_errors) |
74 | |
75 | - if self.options.retry_count: |
76 | + if options.retry_count: |
77 | self.log.info("Looking for errors to auto-retry") |
78 | self.env.resolve_errors( |
79 | - self.options.retry_count, |
80 | - self.options.timeout - time.time() - self.start_time) |
81 | + options.retry_count, |
82 | + options.timeout - time.time() - self.start_time) |
83 | |
84 | === modified file 'deployer/cli.py' |
85 | --- deployer/cli.py 2014-01-23 15:02:57 +0000 |
86 | +++ deployer/cli.py 2014-02-20 12:33:52 +0000 |
87 | @@ -102,6 +102,10 @@ |
88 | help=("Resolve CLI and unit errors via number of retries (default: 0)." |
89 | " Either standalone or in a deployment")) |
90 | parser.add_argument( |
91 | + '--ignore-errors', action='store_true', dest='ignore_errors', |
92 | + help='Proceed with the bundle deployment ignoring units errors. ' |
93 | + 'Unit errors are also automatically ignored if --retry != 0') |
94 | + parser.add_argument( |
95 | "--diff", action="store_true", default=False, |
96 | help=("Generate a delta between a configured deployment and a running" |
97 | " environment.")) |
98 | |
99 | === modified file 'deployer/deployment.py' |
100 | --- deployer/deployment.py 2013-11-20 05:08:15 +0000 |
101 | +++ deployer/deployment.py 2014-02-20 12:33:52 +0000 |
102 | @@ -47,6 +47,10 @@ |
103 | services.sort(self._placement_sort) |
104 | return services |
105 | |
106 | + def get_service_names(self): |
107 | + """Return a sequence of service names for this deployment.""" |
108 | + return self.data.get('services', {}).keys() |
109 | + |
110 | @staticmethod |
111 | def _placement_sort(svc_a, svc_b): |
112 | if svc_a.unit_placement: |
113 | |
114 | === modified file 'deployer/env/go.py' |
115 | --- deployer/env/go.py 2013-11-27 21:05:17 +0000 |
116 | +++ deployer/env/go.py 2014-02-20 12:33:52 +0000 |
117 | @@ -2,14 +2,19 @@ |
118 | import socket |
119 | import time |
120 | |
121 | -from .base import BaseEnvironment |
122 | -from ..utils import ErrorExit |
123 | - |
124 | from jujuclient import ( |
125 | + EnvError, |
126 | Environment as EnvironmentClient, |
127 | UnitErrors, |
128 | - EnvError, |
129 | - WatchWrapper) |
130 | +) |
131 | + |
132 | +from .base import BaseEnvironment |
133 | +from ..utils import ErrorExit |
134 | +from .watchers import ( |
135 | + raise_on_errors, |
136 | + WaitForMachineTermination, |
137 | + WaitForUnits, |
138 | +) |
139 | |
140 | |
141 | class GoEnvironment(BaseEnvironment): |
142 | @@ -45,8 +50,8 @@ |
143 | while True: |
144 | try: |
145 | self.client = EnvironmentClient(self.api_endpoint) |
146 | - except socket.error, e: |
147 | - if e.errno != errno.ETIMEDOUT: |
148 | + except socket.error as err: |
149 | + if err.errno != errno.ETIMEDOUT: |
150 | raise |
151 | continue |
152 | else: |
153 | @@ -60,11 +65,10 @@ |
154 | def get_constraints(self, svc_name): |
155 | try: |
156 | return self.client.get_constraints(svc_name) |
157 | - except EnvError, e: |
158 | - if 'constraints do not apply to subordinate services' in str(e): |
159 | + except EnvError as err: |
160 | + if 'constraints do not apply to subordinate services' in str(err): |
161 | return {} |
162 | - else: |
163 | - raise e |
164 | + raise |
165 | |
166 | def get_cli_status(self): |
167 | status = super(GoEnvironment, self).get_cli_status() |
168 | @@ -94,7 +98,7 @@ |
169 | self.resolve_errors() |
170 | |
171 | # Wait for units |
172 | - self.wait_for_units(timeout, "removed", watch=watch) |
173 | + self.wait_for_units(timeout, goal_state='removed', watch=watch) |
174 | |
175 | # The only value to not terminating is keeping the data on the |
176 | # machines around. |
177 | @@ -182,8 +186,8 @@ |
178 | try: |
179 | self.client.resolved(e_uid, retry=bool(retry_count)) |
180 | self.log.debug(" Resolving error on %s", e_uid) |
181 | - except EnvError, e: |
182 | - if 'already resolved' in e: |
183 | + except EnvError as err: |
184 | + if 'already resolved' in err: |
185 | continue |
186 | |
187 | if not error_units: |
188 | @@ -200,12 +204,13 @@ |
189 | count += 1 |
190 | try: |
191 | self.wait_for_units( |
192 | - timeout=int(w_timeout), watch=True, no_exit=True) |
193 | - except UnitErrors, e: |
194 | + timeout=int(w_timeout), watch=True, |
195 | + on_errors=raise_on_errors(UnitErrors)) |
196 | + except UnitErrors as err: |
197 | if retry_count == count: |
198 | self.log.info( |
199 | " Retry count %d exhausted, but units in error (%s)", |
200 | - retry_count, " ".join(u['Name'] for u in e.errors)) |
201 | + retry_count, " ".join(u['Name'] for u in err.errors)) |
202 | return |
203 | else: |
204 | return |
205 | @@ -216,24 +221,29 @@ |
206 | def status(self): |
207 | return self.client.get_stat() |
208 | |
209 | + def log_errors(self, errors): |
210 | + """Log the given unit errors. |
211 | + |
212 | + This can be used in the WaitForUnits error handling machinery, e.g. |
213 | + see deployer.watchers.log_on_errors. |
214 | + """ |
215 | + messages = [ |
216 | + 'unit: {Name}: machine: {MachineId} agent-state: {Status} ' |
217 | + 'details: {StatusInfo}'.format(**error) for error in errors |
218 | + ] |
219 | + self.log.error( |
220 | + 'The following units had errors:\n {}'.format( |
221 | + ' \n'.join(messages))) |
222 | + |
223 | def wait_for_units( |
224 | - self, timeout, goal_state="started", watch=False, no_exit=False): |
225 | - """Wait for units to reach a given condition. |
226 | - """ |
227 | - callback = watch and self._delta_event_log or None |
228 | - try: |
229 | - self.client.wait_for_units(timeout, goal_state, callback=callback) |
230 | - except UnitErrors, e: |
231 | - error_units = [ |
232 | - "unit: %s: machine: %s agent-state: %s details: %s" % ( |
233 | - u['Name'], u['MachineId'], u['Status'], u['StatusInfo'] |
234 | - ) |
235 | - for u in e.errors] |
236 | - if no_exit: |
237 | - raise |
238 | - self.log.error("The following units had errors:\n %s" % ( |
239 | - " \n".join(error_units))) |
240 | - raise ErrorExit() |
241 | + self, timeout, goal_state="started", watch=False, services=None, |
242 | + on_errors=None): |
243 | + """Wait for units to reach a given condition.""" |
244 | + callback = self._delta_event_log if watch else None |
245 | + watcher = self.client.get_watch(timeout) |
246 | + WaitForUnits( |
247 | + watcher, goal_state=goal_state, |
248 | + services=services, on_errors=on_errors).run(callback) |
249 | |
250 | def _delta_event_log(self, et, ct, d): |
251 | # event type, change type, data |
252 | @@ -260,25 +270,3 @@ |
253 | eps[0]['Relation']['Name'], |
254 | eps[1]['ServiceName'], |
255 | eps[1]['Relation']['Name']) |
256 | - |
257 | - |
258 | -class WaitForMachineTermination(WatchWrapper): |
259 | - |
260 | - def __init__(self, watch, machines): |
261 | - super(WaitForMachineTermination, self).__init__(watch) |
262 | - self.machines = set(machines) |
263 | - self.known = set() |
264 | - |
265 | - def process(self, entity_type, change, data): |
266 | - if entity_type != 'machine': |
267 | - return |
268 | - if change == 'remove' and data['Id'] in self.machines: |
269 | - self.machines.remove(data['Id']) |
270 | - else: |
271 | - self.known.add(data['Id']) |
272 | - |
273 | - def complete(self): |
274 | - for m in self.machines: |
275 | - if m in self.known: |
276 | - return False |
277 | - return True |
278 | |
279 | === modified file 'deployer/env/mem.py' |
280 | --- deployer/env/mem.py 2013-11-29 22:21:54 +0000 |
281 | +++ deployer/env/mem.py 2014-02-20 12:33:52 +0000 |
282 | @@ -168,6 +168,5 @@ |
283 | """ Return all services status """ |
284 | return {'services': self._services} |
285 | |
286 | - def wait_for_units( |
287 | - self, timeout, goal_state="started", watch=False, no_exit=False): |
288 | + def wait_for_units(self, *args, **kwargs): |
289 | pass |
290 | |
291 | === modified file 'deployer/env/py.py' |
292 | --- deployer/env/py.py 2013-07-30 23:39:51 +0000 |
293 | +++ deployer/env/py.py 2014-02-20 12:33:52 +0000 |
294 | @@ -77,9 +77,26 @@ |
295 | def status(self): |
296 | return self.get_cli_status() |
297 | |
298 | + def log_errors(self, errors): |
299 | + """Log the given unit errors. |
300 | + |
301 | + This can be used in the WaitForUnits error handling machinery, e.g. |
302 | + see deployer.watchers.log_on_errors. |
303 | + """ |
304 | + messages = [ |
305 | + 'unit: {unit[name]}: machine: {unit[machine]} ' |
306 | + 'agent-state: {unit[agent-state]}'.format(unit=error) |
307 | + for error in errors |
308 | + ] |
309 | + self.log.error( |
310 | + 'The following units had errors:\n {}'.format( |
311 | + ' \n'.join(messages))) |
312 | + |
313 | def wait_for_units( |
314 | - self, timeout, goal_state="started", watch=False, no_exit=False): |
315 | - |
316 | + self, timeout, goal_state="started", watch=False, services=None, |
317 | + on_errors=None): |
318 | + # Note that the services keyword argument is ignored in this pyJuju |
319 | + # implementation: we wait for all the units in the environment. |
320 | max_time = time.time() + timeout |
321 | while max_time > time.time(): |
322 | status = self.status() |
323 | @@ -111,15 +128,4 @@ |
324 | if not pending and not errors: |
325 | break |
326 | if errors: |
327 | - if no_exit: |
328 | - raise UnitErrors(errors) |
329 | - else: |
330 | - error_units = [ |
331 | - "unit: %s: machine: %s agent-state: %s" % ( |
332 | - u["name"], u["machine"], u["agent-state"] |
333 | - ) |
334 | - for u in errors] |
335 | - self.log.error( |
336 | - "The following units had errors:\n %s" % ( |
337 | - " \n".join(error_units))) |
338 | - raise ErrorExit() |
339 | + on_errors(errors) |
340 | |
341 | === added file 'deployer/env/watchers.py' |
342 | --- deployer/env/watchers.py 1970-01-01 00:00:00 +0000 |
343 | +++ deployer/env/watchers.py 2014-02-20 12:33:52 +0000 |
344 | @@ -0,0 +1,110 @@ |
345 | +"""A collection of juju-core environment watchers.""" |
346 | + |
347 | +from jujuclient import WatchWrapper |
348 | + |
349 | +from ..utils import ErrorExit |
350 | + |
351 | + |
352 | +class WaitForMachineTermination(WatchWrapper): |
353 | + """Wait until the given machines are terminated.""" |
354 | + |
355 | + def __init__(self, watch, machines): |
356 | + super(WaitForMachineTermination, self).__init__(watch) |
357 | + self.machines = set(machines) |
358 | + self.known = set() |
359 | + |
360 | + def process(self, entity_type, change, data): |
361 | + if entity_type != 'machine': |
362 | + return |
363 | + if change == 'remove' and data['Id'] in self.machines: |
364 | + self.machines.remove(data['Id']) |
365 | + else: |
366 | + self.known.add(data['Id']) |
367 | + |
368 | + def complete(self): |
369 | + for m in self.machines: |
370 | + if m in self.known: |
371 | + return False |
372 | + return True |
373 | + |
374 | + |
375 | +class WaitForUnits(WatchWrapper): |
376 | + """Wait for units of the environment to reach a particular goal state. |
377 | + |
378 | + If services are provided, only consider the units belonging to the given |
379 | + services. |
380 | + If the on_errors callable is provided, call the given function each time a |
381 | + change set is processed and a new unit is found in an error state. The |
382 | + callable is called passing a list of units' data corresponding to the units |
383 | + in an error state. |
384 | + """ |
385 | + def __init__( |
386 | + self, watch, goal_state='started', services=None, on_errors=None): |
387 | + super(WaitForUnits, self).__init__(watch) |
388 | + self.goal_state = goal_state |
389 | + self.services = services |
390 | + self.on_errors = on_errors |
391 | + # The units dict maps unit names to units data. |
392 | + self.units = {} |
393 | + # The units_in_error list contains the names of the units in error. |
394 | + self.units_in_error = [] |
395 | + |
396 | + def process(self, entity, action, data): |
397 | + if entity != 'unit': |
398 | + return |
399 | + if (self.services is None) or (data['Service'] in self.services): |
400 | + unit_name = data['Name'] |
401 | + if action == 'remove' and unit_name in self.units: |
402 | + del self.units[unit_name] |
403 | + else: |
404 | + self.units[unit_name] = data |
405 | + |
406 | + def complete(self): |
407 | + ready = True |
408 | + new_errors = [] |
409 | + goal_state = self.goal_state |
410 | + on_errors = self.on_errors |
411 | + units_in_error = self.units_in_error |
412 | + for unit_name, data in self.units.items(): |
413 | + status = data['Status'] |
414 | + if status == 'error': |
415 | + if unit_name not in units_in_error: |
416 | + units_in_error.append(unit_name) |
417 | + new_errors.append(data) |
418 | + elif status != goal_state: |
419 | + ready = False |
420 | + if new_errors and goal_state != 'removed' and callable(on_errors): |
421 | + on_errors(new_errors) |
422 | + return ready |
423 | + |
424 | + |
425 | +def log_on_errors(env): |
426 | + """Return a function receiving errors and logging them. |
427 | + |
428 | + The resulting function is suitable to be used as the on_errors callback |
429 | + for WaitForUnits (see above). |
430 | + """ |
431 | + return env.log_errors |
432 | + |
433 | + |
434 | +def exit_on_errors(env): |
435 | + """Return a function receiving errors, logging them and exiting the app. |
436 | + |
437 | + The resulting function is suitable to be used as the on_errors callback |
438 | + for WaitForUnits (see above). |
439 | + """ |
440 | + def callback(errors): |
441 | + log_on_errors(env)(errors) |
442 | + raise ErrorExit() |
443 | + return callback |
444 | + |
445 | + |
446 | +def raise_on_errors(exception): |
447 | + """Return a function receiving errors and raising the given exception. |
448 | + |
449 | + The resulting function is suitable to be used as the on_errors callback |
450 | + for WaitForUnits (see above). |
451 | + """ |
452 | + def callback(errors): |
453 | + raise exception(errors) |
454 | + return callback |
455 | |
456 | === modified file 'deployer/guiserver.py' |
457 | --- deployer/guiserver.py 2014-02-12 21:00:47 +0000 |
458 | +++ deployer/guiserver.py 2014-02-20 12:33:52 +0000 |
459 | @@ -12,6 +12,7 @@ |
460 | import os |
461 | |
462 | from deployer.action.importer import Importer |
463 | +from deployer.cli import setup_parser |
464 | from deployer.deployment import Deployment |
465 | from deployer.env.gui import GUIEnvironment |
466 | from deployer.utils import ( |
467 | @@ -26,6 +27,13 @@ |
468 | JUJU_HOME = '/var/lib/juju-gui/juju-home' |
469 | |
470 | |
471 | +def get_default_guiserver_options(): |
472 | + """Return the default importer options used by the GUI server.""" |
473 | + # Options used by the juju-deployer. The defaults work for us, except for |
474 | + # the ignore_errors flag. |
475 | + return setup_parser().parse_args(['--ignore-errors']) |
476 | + |
477 | + |
478 | class GUIDeployment(Deployment): |
479 | """Handle bundle deployments requested by the GUI server.""" |
480 | |
481 | |
482 | === modified file 'deployer/tests/test_deployment.py' |
483 | --- deployer/tests/test_deployment.py 2014-01-23 12:55:11 +0000 |
484 | +++ deployer/tests/test_deployment.py 2014-02-20 12:33:52 +0000 |
485 | @@ -148,3 +148,11 @@ |
486 | self.assertEqual( |
487 | [('keystone', 'mysql'), ('glance', 'mysql'), |
488 | ('nova-compute', 'mysql')], list(d.get_relations())) |
489 | + |
490 | + def test_getting_service_names(self): |
491 | + # It is possible to retrieve the service names. |
492 | + deployment = self.get_named_deployment("stack-placement.yaml", "stack") |
493 | + service_names = deployment.get_service_names() |
494 | + expected_service_names = [ |
495 | + 'ceph', 'mysql', 'nova-compute', 'quantum', 'semper', 'verity'] |
496 | + self.assertEqual(set(expected_service_names), set(service_names)) |
497 | |
498 | === modified file 'deployer/tests/test_guiserver.py' |
499 | --- deployer/tests/test_guiserver.py 2014-02-20 01:20:23 +0000 |
500 | +++ deployer/tests/test_guiserver.py 2014-02-20 12:33:52 +0000 |
501 | @@ -10,14 +10,57 @@ |
502 | import mock |
503 | import yaml |
504 | |
505 | -from deployer import ( |
506 | - cli, |
507 | - guiserver, |
508 | -) |
509 | +from deployer import guiserver |
510 | from deployer.feedback import Feedback |
511 | from deployer.tests.base import TEST_OFFLINE |
512 | |
513 | |
514 | +class TestGetDefaultGuiserverOptions(unittest.TestCase): |
515 | + |
516 | + def setUp(self): |
517 | + self.options = guiserver.get_default_guiserver_options() |
518 | + |
519 | + def test_option_keys(self): |
520 | + # All the required options are returned. |
521 | + # When adding/modifying options, ensure the defaults are sane for us. |
522 | + expected_keys = set([ |
523 | + 'bootstrap', 'branch_only', 'configs', 'debug', 'deploy_delay', |
524 | + 'deployment', 'description', 'destroy_services', 'diff', |
525 | + 'find_service', 'ignore_errors', 'juju_env', 'list_deploys', |
526 | + 'no_local_mods', 'overrides', 'rel_wait', 'retry_count', 'series', |
527 | + 'terminate_machines', 'timeout', 'update_charms', 'verbose', |
528 | + 'watch' |
529 | + ]) |
530 | + self.assertEqual(expected_keys, set(self.options.__dict__.keys())) |
531 | + |
532 | + def test_option_values(self): |
533 | + # The options values are suitable to be used by the GUI server. |
534 | + # When adding/modifying options, ensure the defaults are sane for us. |
535 | + options = self.options |
536 | + self.assertFalse(options.bootstrap) |
537 | + self.assertFalse(options.branch_only) |
538 | + self.assertIsNone(options.configs) |
539 | + self.assertFalse(options.debug) |
540 | + self.assertEqual(0, options.deploy_delay) |
541 | + self.assertIsNone(options.deployment) |
542 | + self.assertFalse(options.destroy_services) |
543 | + self.assertFalse(options.diff) |
544 | + self.assertIsNone(options.find_service) |
545 | + self.assertTrue(options.ignore_errors) |
546 | + self.assertEqual(os.getenv("JUJU_ENV"), options.juju_env) |
547 | + self.assertFalse(options.list_deploys) |
548 | + self.assertTrue(options.no_local_mods) |
549 | + self.assertIsNone(options.overrides) |
550 | + self.assertEqual(60, options.rel_wait) |
551 | + self.assertEqual(0, options.retry_count) |
552 | + self.assertIsNone(options.series) |
553 | + self.assertFalse(options.terminate_machines) |
554 | + self.assertEqual(2700, options.timeout) |
555 | + self.assertFalse(options.update_charms) |
556 | + self.assertFalse(options.verbose) |
557 | + self.assertFalse(options.watch) |
558 | + |
559 | + |
560 | class TestDeploymentError(unittest.TestCase): |
561 | |
562 | def test_error(self): |
563 | @@ -152,7 +195,7 @@ |
564 | class TestImportBundle(DeployerFunctionsTestMixin, unittest.TestCase): |
565 | |
566 | # The options attribute simulates the options passed to the Importer. |
567 | - options = cli.setup_parser().parse_args([]) |
568 | + options = guiserver.get_default_guiserver_options() |
569 | |
570 | @contextmanager |
571 | def patch_juju_home(self): |
572 | @@ -274,30 +317,3 @@ |
573 | 'Invalid config charm cs:precise/mysql-28 bad=wolf', |
574 | ]) |
575 | self.assertEqual(expected_errors, set(cm.exception.errors)) |
576 | - |
577 | - def test_options(self, mock_environment): |
578 | - # Ensure the default options are sane for us. |
579 | - expected = argparse.Namespace( |
580 | - bootstrap=False, |
581 | - branch_only=False, |
582 | - configs=None, |
583 | - debug=False, |
584 | - deploy_delay=0, |
585 | - deployment=None, |
586 | - description=False, |
587 | - destroy_services=False, |
588 | - diff=False, |
589 | - find_service=None, |
590 | - juju_env=os.environ.get("JUJU_ENV"), |
591 | - list_deploys=False, |
592 | - no_local_mods=True, |
593 | - overrides=None, |
594 | - rel_wait=60, |
595 | - retry_count=0, |
596 | - series=None, |
597 | - terminate_machines=False, |
598 | - timeout=2700, |
599 | - update_charms=False, |
600 | - verbose=False, |
601 | - watch=False) |
602 | - self.assertEqual(expected, self.options) |
603 | |
604 | === modified file 'deployer/tests/test_pyenv.py' |
605 | --- deployer/tests/test_pyenv.py 2013-07-22 15:29:31 +0000 |
606 | +++ deployer/tests/test_pyenv.py 2014-02-20 12:33:52 +0000 |
607 | @@ -1,14 +1,14 @@ |
608 | - |
609 | import StringIO |
610 | |
611 | +from jujuclient import UnitErrors |
612 | + |
613 | +from .base import Base |
614 | +from deployer.env import watchers |
615 | from deployer.env.py import PyEnvironment |
616 | from deployer.errors import UnitErrors |
617 | from deployer.utils import setup_logging, ErrorExit |
618 | |
619 | |
620 | -from .base import Base |
621 | - |
622 | - |
623 | class FakePyEnvironment(PyEnvironment): |
624 | |
625 | def __init__(self, name, status): |
626 | @@ -38,16 +38,15 @@ |
627 | }, |
628 | }, |
629 | }) |
630 | - try: |
631 | - env.wait_for_units(timeout=240, no_exit=True) |
632 | - except UnitErrors, e: |
633 | - self.assertEqual(1, len(e.errors)) |
634 | - unit = e.errors[0] |
635 | - self.assertEqual("wordpress/0", unit["name"]) |
636 | - self.assertEqual("install-error", unit["agent-state"]) |
637 | - self.assertEqual(1, unit["machine"]) |
638 | - else: |
639 | - self.fail("Did not get expected exception") |
640 | + with self.assertRaises(UnitErrors) as cm: |
641 | + env.wait_for_units( |
642 | + timeout=240, on_errors=watchers.raise_on_errors(UnitErrors)) |
643 | + errors = cm.exception.errors |
644 | + self.assertEqual(1, len(errors)) |
645 | + unit = errors[0] |
646 | + self.assertEqual("wordpress/0", unit["name"]) |
647 | + self.assertEqual("install-error", unit["agent-state"]) |
648 | + self.assertEqual(1, unit["machine"]) |
649 | |
650 | def test_wait_for_units_error_exit(self): |
651 | env = FakePyEnvironment( |
652 | @@ -62,16 +61,13 @@ |
653 | }, |
654 | }, |
655 | }) |
656 | - try: |
657 | - env.wait_for_units(timeout=240, no_exit=False) |
658 | - except ErrorExit: |
659 | - self.assertIn('The following units had errors:', |
660 | - self.output.getvalue()) |
661 | - self.assertIn('unit: wordpress/0: machine: 1 ' |
662 | - 'agent-state: install-error', |
663 | - self.output.getvalue()) |
664 | - else: |
665 | - self.fail("Did not get expected exception") |
666 | + with self.assertRaises(ErrorExit): |
667 | + env.wait_for_units( |
668 | + timeout=240, on_errors=watchers.exit_on_errors(env)) |
669 | + output = self.output.getvalue() |
670 | + self.assertIn('The following units had errors:', output) |
671 | + self.assertIn( |
672 | + 'unit: wordpress/0: machine: 1 agent-state: install-error', output) |
673 | |
674 | def test_wait_for_units_sub_error_no_exit(self): |
675 | env = FakePyEnvironment( |
676 | @@ -91,16 +87,15 @@ |
677 | }, |
678 | }, |
679 | }) |
680 | - try: |
681 | - env.wait_for_units(timeout=240, no_exit=True) |
682 | - except UnitErrors, e: |
683 | - self.assertEqual(1, len(e.errors)) |
684 | - unit = e.errors[0] |
685 | - self.assertEqual("nrpe/0", unit["name"]) |
686 | - self.assertEqual("install-error", unit["agent-state"]) |
687 | - self.assertEqual(1, unit["machine"]) |
688 | - else: |
689 | - self.fail("Did not get expected exception") |
690 | + with self.assertRaises(UnitErrors) as cm: |
691 | + env.wait_for_units( |
692 | + timeout=240, on_errors=watchers.raise_on_errors(UnitErrors)) |
693 | + errors = cm.exception.errors |
694 | + self.assertEqual(1, len(errors)) |
695 | + unit = errors[0] |
696 | + self.assertEqual("nrpe/0", unit["name"]) |
697 | + self.assertEqual("install-error", unit["agent-state"]) |
698 | + self.assertEqual(1, unit["machine"]) |
699 | |
700 | def test_wait_for_units_no_error_no_exit(self): |
701 | env = FakePyEnvironment( |
702 | @@ -121,9 +116,10 @@ |
703 | }, |
704 | }) |
705 | try: |
706 | - env.wait_for_units(timeout=240, no_exit=True) |
707 | - except UnitErrors, e: |
708 | - self.fail("Unexpected exception: %s" % e) |
709 | + env.wait_for_units( |
710 | + timeout=240, on_errors=watchers.raise_on_errors(UnitErrors)) |
711 | + except UnitErrors as err: |
712 | + self.fail("Unexpected exception: %s" % err) |
713 | |
714 | def test_wait_for_units_relation_error_no_exit(self): |
715 | env = FakePyEnvironment( |
716 | @@ -141,16 +137,15 @@ |
717 | }, |
718 | }, |
719 | }) |
720 | - try: |
721 | - env.wait_for_units(timeout=240, no_exit=True) |
722 | - except UnitErrors, e: |
723 | - self.assertEqual(1, len(e.errors)) |
724 | - unit = e.errors[0] |
725 | - self.assertEqual("wordpress/0", unit["name"]) |
726 | - self.assertEqual("relation-error: nrpe", unit["agent-state"]) |
727 | - self.assertEqual(1, unit["machine"]) |
728 | - else: |
729 | - self.fail("Did not get expected exception") |
730 | + with self.assertRaises(UnitErrors) as cm: |
731 | + env.wait_for_units( |
732 | + timeout=240, on_errors=watchers.raise_on_errors(UnitErrors)) |
733 | + errors = cm.exception.errors |
734 | + self.assertEqual(1, len(errors)) |
735 | + unit = errors[0] |
736 | + self.assertEqual("wordpress/0", unit["name"]) |
737 | + self.assertEqual("relation-error: nrpe", unit["agent-state"]) |
738 | + self.assertEqual(1, unit["machine"]) |
739 | |
740 | def test_wait_for_units_subordinate_no_unit_no_error_no_exit(self): |
741 | env = FakePyEnvironment( |
742 | @@ -163,6 +158,7 @@ |
743 | }, |
744 | }) |
745 | try: |
746 | - env.wait_for_units(timeout=240, no_exit=True) |
747 | - except UnitErrors, e: |
748 | - self.fail("Unexpected exception: %s" % e) |
749 | + env.wait_for_units( |
750 | + timeout=240, on_errors=watchers.raise_on_errors(UnitErrors)) |
751 | + except UnitErrors as err: |
752 | + self.fail("Unexpected exception: %s" % err) |
753 | |
754 | === added file 'deployer/tests/test_watchers.py' |
755 | --- deployer/tests/test_watchers.py 1970-01-01 00:00:00 +0000 |
756 | +++ deployer/tests/test_watchers.py 2014-02-20 12:33:52 +0000 |
757 | @@ -0,0 +1,146 @@ |
758 | +"""Tests juju-core environment watchers.""" |
759 | + |
760 | +import unittest |
761 | + |
762 | +import mock |
763 | + |
764 | +from deployer.env import watchers |
765 | +from deployer.utils import ErrorExit |
766 | + |
767 | + |
768 | +class TestWaitForUnits(unittest.TestCase): |
769 | + |
770 | + def make_watch(self, *changesets): |
771 | + """Create and return a mock watcher returning the given change sets.""" |
772 | + watch = mock.MagicMock() |
773 | + watch.__iter__.return_value = changesets |
774 | + return watch |
775 | + |
776 | + def make_change(self, unit_name, status): |
777 | + """Create and return a juju-core mega-watcher unit change.""" |
778 | + service = unit_name.split('/')[0] |
779 | + return {'Name': unit_name, 'Service': service, 'Status': status} |
780 | + |
781 | + def test_success(self): |
782 | + # It is possible to watch for units to be started. |
783 | + watch = self.make_watch( |
784 | + [('unit', 'change', self.make_change('django/0', 'pending')), |
785 | + ('unit', 'change', self.make_change('haproxy/1', 'pending'))], |
786 | + [('unit', 'change', self.make_change('django/0', 'started')), |
787 | + ('unit', 'change', self.make_change('haproxy/1', 'started'))], |
788 | + ) |
789 | + watcher = watchers.WaitForUnits(watch) |
790 | + callback = mock.Mock() |
791 | + watcher.run(callback) |
792 | + # The callback has been called once for each change in the change sets, |
793 | + # excluding the initial state. |
794 | + self.assertEqual(2, callback.call_count) |
795 | + # The watcher has been properly stopped. |
796 | + watch.stop.assert_called_once_with() |
797 | + |
798 | + def test_errors_handling(self): |
799 | + # Errors can be handled providing an on_errors callable. |
800 | + watch = self.make_watch( |
801 | + [('unit', 'change', self.make_change('django/0', 'pending')), |
802 | + ('unit', 'change', self.make_change('django/42', 'pending')), |
803 | + ('unit', 'change', self.make_change('haproxy/1', 'pending'))], |
804 | + [('unit', 'change', self.make_change('django/0', 'pending')), |
805 | + ('unit', 'change', self.make_change('django/42', 'error')), |
806 | + ('unit', 'change', self.make_change('haproxy/1', 'error'))], |
807 | + [('unit', 'change', self.make_change('django/0', 'error')), |
808 | + ('unit', 'change', self.make_change('django/42', 'error')), |
809 | + ('unit', 'change', self.make_change('haproxy/1', 'error'))], |
810 | + ) |
811 | + on_errors = mock.Mock() |
812 | + watcher = watchers.WaitForUnits(watch, on_errors=on_errors) |
813 | + watcher.run() |
814 | + # The watcher has been properly stopped. |
815 | + watch.stop.assert_called_once_with() |
816 | + # The errors handler has been called once for each changeset containing |
817 | + # errors. |
818 | + self.assertEqual(2, on_errors.call_count) |
819 | + on_errors.assert_has_calls([ |
820 | + mock.call([ |
821 | + {'Status': 'error', 'Name': 'django/42', 'Service': 'django'}, |
822 | + {'Status': 'error', 'Name': 'haproxy/1', 'Service': 'haproxy'} |
823 | + ]), |
824 | + mock.call([ |
825 | + {'Status': 'error', 'Name': 'django/0', 'Service': 'django'}]), |
826 | + ]) |
827 | + |
828 | + def test_specific_services(self): |
829 | + # It is possible to only watch units belonging to specific services. |
830 | + watch = self.make_watch( |
831 | + [('unit', 'change', self.make_change('django/0', 'pending')), |
832 | + ('unit', 'change', self.make_change('django/42', 'pending')), |
833 | + ('unit', 'change', self.make_change('haproxy/1', 'pending')), |
834 | + ('unit', 'change', self.make_change('haproxy/47', 'pending'))], |
835 | + [('unit', 'change', self.make_change('django/0', 'pending')), |
836 | + ('unit', 'change', self.make_change('django/42', 'started')), |
837 | + ('unit', 'change', self.make_change('haproxy/1', 'error')), |
838 | + ('unit', 'change', self.make_change('haproxy/47', 'pending'))], |
839 | + [('unit', 'change', self.make_change('django/0', 'error')), |
840 | + ('unit', 'change', self.make_change('django/42', 'started')), |
841 | + ('unit', 'change', self.make_change('haproxy/1', 'error')), |
842 | + ('unit', 'change', self.make_change('haproxy/47', 'pending'))], |
843 | + ) |
844 | + on_errors = mock.Mock() |
845 | + watcher = watchers.WaitForUnits( |
846 | + watch, services=['django'], on_errors=on_errors) |
847 | + watcher.run() |
848 | + # The watcher has been properly stopped, even if haproxy/47 is pending. |
849 | + watch.stop.assert_called_once_with() |
850 | + # The errors handler has been called for the django error, not for the |
851 | + # haproxy one. |
852 | + on_errors.assert_called_once_with( |
853 | + [{'Status': 'error', 'Name': 'django/0', 'Service': 'django'}]) |
854 | + |
855 | + def test_goal_states(self): |
856 | + # It is possible to watch for the given goal state |
857 | + watch = self.make_watch( |
858 | + [('unit', 'change', self.make_change('django/0', 'pending')), |
859 | + ('unit', 'change', self.make_change('haproxy/1', 'pending'))], |
860 | + ) |
861 | + watcher = watchers.WaitForUnits(watch, goal_state='pending') |
862 | + callback = mock.Mock() |
863 | + watcher.run(callback) |
864 | + # Since all the units are already pending, the watcher has been |
865 | + # properly stopped. |
866 | + watch.stop.assert_called_once_with() |
867 | + |
868 | + |
869 | +class TestLogOnErrors(unittest.TestCase): |
870 | + |
871 | + def setUp(self): |
872 | + # Set up a mock environment. |
873 | + self.env = mock.Mock() |
874 | + |
875 | + def test_returned_callable(self): |
876 | + # The returned function uses the env to log errors. |
877 | + callback = watchers.log_on_errors(self.env) |
878 | + self.assertEqual(self.env.log_errors, callback) |
879 | + |
880 | + |
881 | +class TestExitOnErrors(unittest.TestCase): |
882 | + |
883 | + def setUp(self): |
884 | + # Set up a mock environment. |
885 | + self.env = mock.Mock() |
886 | + |
887 | + def test_returned_callable(self): |
888 | + # The returned function uses the env to log errors and then exits the |
889 | + # application. |
890 | + callback = watchers.exit_on_errors(self.env) |
891 | + with self.assertRaises(ErrorExit): |
892 | + callback('bad wolf') |
893 | + self.env.log_errors.assert_called_once_with('bad wolf') |
894 | + |
895 | + |
896 | +class TestRaiseOnErrors(unittest.TestCase): |
897 | + |
898 | + def test_returned_callable(self): |
899 | + # The returned function raises the given exception passing the errors. |
900 | + callback = watchers.raise_on_errors(ValueError) |
901 | + with self.assertRaises(ValueError) as cm: |
902 | + callback('bad wolf') |
903 | + self.assertEqual('bad wolf', bytes(cm.exception)) |
Might be nice to mention in the help for '--ignore-errors' the relationship to retry_count.
Otherwise this branch looks really good. I'm -0 on changes like 'options = self.options' in existing code due to the noise for no real benefit, but "meh" as you say. :)