Merge lp:~frankban/juju-quickstart/handle-machine-errors into lp:juju-quickstart
- handle-machine-errors
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 47 |
Proposed branch: | lp:~frankban/juju-quickstart/handle-machine-errors |
Merge into: | lp:juju-quickstart |
Diff against target: |
982 lines (+618/-182) 7 files modified
quickstart/app.py (+39/-34) quickstart/manage.py (+6/-4) quickstart/tests/test_app.py (+189/-84) quickstart/tests/test_utils.py (+0/-48) quickstart/tests/test_watchers.py (+266/-0) quickstart/utils.py (+0/-12) quickstart/watchers.py (+118/-0) |
To merge this branch: | bzr merge lp:~frankban/juju-quickstart/handle-machine-errors |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju GUI Hackers | Pending | ||
Review via email:
|
Commit message
Description of the change
Improve machine errors handling.
Quickstart no longer hangs when the GUI
machine goes in an "error" state and, as
a consequence, the unit is forever in
a "pending" state.
Also fixed the message printed at the
end of the process suggesting how to
destroy the environment
(sudo is used where required).
Tests: `make check`.
QA: I already asked Rick to reproduce the trusty/LXC
errors he encountered QAing my previous branch.
The GUI no longer hangs.
I'd also appreciate any other suggestion and additional
QA, given that this branch is a good candidate for 1.0.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Francesco Banconi (frankban) wrote : | # |
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Brad Crittenden (bac) wrote : | # |
Code LGTM. Have not yet done QA.
https:/
File quickstart/app.py (right):
https:/
quickstart/
loop here.
You don't sound convinced. Any chance that assumption is wrong? The
penalty for looping over the rest non-matching items is trivial. But if
you're sure, maybe keep this logic but change the comment to be more
confident.
https:/
quickstart/
change for each
As above.
- 54. By Francesco Banconi
-
Changes as per review.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Francesco Banconi (frankban) wrote : | # |
Please take a look.
https:/
File quickstart/app.py (right):
https:/
quickstart/
loop here.
On 2014/01/17 15:20:15, bac wrote:
> You don't sound convinced. Any chance that assumption is wrong? The
penalty
> for looping over the rest non-matching items is trivial. But if
you're sure,
> maybe keep this logic but change the comment to be more confident.
Done.
https:/
quickstart/
change for each
On 2014/01/17 15:20:15, bac wrote:
> As above.
Done.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Richard Harding (rharding) wrote : | # |
LGTM thanks for the updates and a lot more tests.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Brad Crittenden (bac) wrote : | # |
qa-ok, modulo the issue we talked about on IRC.
- 55. By Francesco Banconi
-
Fix typo.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Francesco Banconi (frankban) wrote : | # |
*** Submitted:
Improve machine errors handling.
Quickstart no longer hangs when the GUI
machine goes in an "error" state and, as
a consequence, the unit is forever in
a "pending" state.
Also fixed the message printed at the
end of the process suggesting how to
destroy the environment
(sudo is used where required).
Tests: `make check`.
QA: I already asked Rick to reproduce the trusty/LXC
errors he encountered QAing my previous branch.
The GUI no longer hangs.
I'd also appreciate any other suggestion and additional
QA, given that this branch is a good candidate for 1.0.
R=bac, rharding
CC=
https:/
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Francesco Banconi (frankban) wrote : | # |
Thank you!
Preview Diff
1 | === modified file 'quickstart/app.py' | |||
2 | --- quickstart/app.py 2014-01-08 21:25:36 +0000 | |||
3 | +++ quickstart/app.py 2014-01-17 18:31:35 +0000 | |||
4 | @@ -21,7 +21,6 @@ | |||
5 | 21 | unicode_literals, | 21 | unicode_literals, |
6 | 22 | ) | 22 | ) |
7 | 23 | 23 | ||
8 | 24 | import functools | ||
9 | 25 | import json | 24 | import json |
10 | 26 | import logging | 25 | import logging |
11 | 27 | import os | 26 | import os |
12 | @@ -34,6 +33,7 @@ | |||
13 | 34 | juju, | 33 | juju, |
14 | 35 | settings, | 34 | settings, |
15 | 36 | utils, | 35 | utils, |
16 | 36 | watchers, | ||
17 | 37 | ) | 37 | ) |
18 | 38 | 38 | ||
19 | 39 | 39 | ||
20 | @@ -427,55 +427,60 @@ | |||
21 | 427 | 427 | ||
22 | 428 | 428 | ||
23 | 429 | def watch(env, unit_name): | 429 | def watch(env, unit_name): |
25 | 430 | """Start watching the given unit. | 430 | """Start watching the given unit and the machine the unit belongs to. |
26 | 431 | 431 | ||
28 | 432 | Output a human readable messages from each time a relevant change is found. | 432 | Output a human readable message each time a relevant change is found. |
29 | 433 | 433 | ||
30 | 434 | The following changes are considered relevant for a healthy unit: | 434 | The following changes are considered relevant for a healthy unit: |
31 | 435 | - the machine is pending; | ||
32 | 435 | - the unit is pending; | 436 | - the unit is pending; |
33 | 437 | - the machine is started; | ||
34 | 436 | - the unit is reachable; | 438 | - the unit is reachable; |
35 | 437 | - the unit is installed; | 439 | - the unit is installed; |
36 | 438 | - the unit is started. | 440 | - the unit is started. |
37 | 439 | 441 | ||
38 | 440 | Stop watching and return the unit public address when the unit is started. | 442 | Stop watching and return the unit public address when the unit is started. |
39 | 441 | Raise a ProgramExit if the API server returns an error response, or if | 443 | Raise a ProgramExit if the API server returns an error response, or if |
41 | 442 | the service unit is removed or in error. | 444 | either the service unit or the machine is removed or in error. |
42 | 443 | """ | 445 | """ |
46 | 444 | processor = functools.partial(utils.unit_changes, unit_name) | 446 | address = unit_status = machine_id = machine_status = '' |
47 | 445 | watcher = env.watch_changes(processor) | 447 | watcher = env.watch_changes(watchers.unit_machine_changes) |
45 | 446 | address = current_status = '' | ||
48 | 447 | while True: | 448 | while True: |
49 | 448 | try: | 449 | try: |
51 | 449 | entity, action, data = watcher.next() | 450 | unit_changes, machine_changes = watcher.next() |
52 | 450 | except jujuclient.EnvError as err: | 451 | except jujuclient.EnvError as err: |
53 | 451 | raise ProgramExit( | 452 | raise ProgramExit( |
54 | 452 | 'bad API server response: {}'.format(err.message)) | 453 | 'bad API server response: {}'.format(err.message)) |
81 | 453 | # Exit with an error if the unit is removed. | 454 | # Process unit changes: |
82 | 454 | if action == 'remove': | 455 | for action, data in unit_changes: |
83 | 455 | raise ProgramExit('{} unexpectedly removed'.format(unit_name)) | 456 | if data['Name'] == unit_name: |
84 | 456 | # Exit with an error if the unit is in an error state. | 457 | try: |
85 | 457 | status = data['Status'] | 458 | data = watchers.parse_unit_change( |
86 | 458 | if 'error' in status: | 459 | action, data, unit_status, address) |
87 | 459 | msg = '{} is in an error state: {}: {}'.format( | 460 | except ValueError as err: |
88 | 460 | unit_name, status, data['StatusInfo']) | 461 | raise ProgramExit(bytes(err)) |
89 | 461 | raise ProgramExit(msg) | 462 | unit_status, address, machine_id = data |
90 | 462 | # Notify when the unit becomes reachable. | 463 | if address and unit_status == 'started': |
91 | 463 | if not address: | 464 | # We can exit this loop. |
92 | 464 | address = data['PublicAddress'] | 465 | return address |
93 | 465 | if address: | 466 | # The mega-watcher contains a single change for each specific |
94 | 466 | print('setting up {} on {}'.format(unit_name, address)) | 467 | # unit. For this reason, we can exit the for loop here. |
95 | 467 | # Notify status changes. | 468 | break |
96 | 468 | if status != current_status: | 469 | if not machine_id: |
97 | 469 | if status == 'pending': | 470 | # No need to process machine changes: we still don't know what |
98 | 470 | print('{} deployment is pending'.format(unit_name)) | 471 | # machine the unit belongs to. |
99 | 471 | elif status == 'installed': | 472 | continue |
100 | 472 | print('{} is installed'.format(unit_name)) | 473 | # Process machine changes. |
101 | 473 | elif address and status == 'started': | 474 | for action, data in machine_changes: |
102 | 474 | # We can exit this loop. | 475 | if data['Id'] == machine_id: |
103 | 475 | print('{} is ready on machine {}'.format( | 476 | try: |
104 | 476 | unit_name, data['MachineId'])) | 477 | machine_status = watchers.parse_machine_change( |
105 | 477 | return address | 478 | action, data, machine_status) |
106 | 478 | current_status = status | 479 | except ValueError as err: |
107 | 480 | raise ProgramExit(bytes(err)) | ||
108 | 481 | # The mega-watcher contains a single change for each specific | ||
109 | 482 | # machine. For this reason, we can exit the for loop here. | ||
110 | 483 | break | ||
111 | 479 | 484 | ||
112 | 480 | 485 | ||
113 | 481 | def deploy_bundle(env, bundle_yaml, bundle_name, bundle_id): | 486 | def deploy_bundle(env, bundle_yaml, bundle_name, bundle_id): |
114 | 482 | 487 | ||
115 | === modified file 'quickstart/manage.py' | |||
116 | --- quickstart/manage.py 2014-01-16 12:41:47 +0000 | |||
117 | +++ quickstart/manage.py 2014-01-17 18:31:35 +0000 | |||
118 | @@ -413,14 +413,16 @@ | |||
119 | 413 | 413 | ||
120 | 414 | # Handle bundle deployment. | 414 | # Handle bundle deployment. |
121 | 415 | if options.bundle is not None: | 415 | if options.bundle is not None: |
124 | 416 | print('deploying the bundle {} with the following services: {}'.format( | 416 | services = ', '.join(options.bundle_services) |
125 | 417 | options.bundle_name, ', '.join(options.bundle_services))) | 417 | print('requesting a deployment of the {} bundle with the following ' |
126 | 418 | 'services:\n {}'.format(options.bundle_name, services)) | ||
127 | 418 | # We need to connect to an API WebSocket server supporting bundle | 419 | # We need to connect to an API WebSocket server supporting bundle |
128 | 419 | # deployments. The GUI builtin server, listening on the Juju GUI | 420 | # deployments. The GUI builtin server, listening on the Juju GUI |
129 | 420 | # address, exposes an API suitable for deploying bundles. | 421 | # address, exposes an API suitable for deploying bundles. |
130 | 421 | app.deploy_bundle( | 422 | app.deploy_bundle( |
131 | 422 | gui_env, options.bundle_yaml, options.bundle_name, | 423 | gui_env, options.bundle_yaml, options.bundle_name, |
132 | 423 | options.bundle_id) | 424 | options.bundle_id) |
133 | 425 | print('bundle deployment request accepted') | ||
134 | 424 | 426 | ||
135 | 425 | if options.open_browser: | 427 | if options.open_browser: |
136 | 426 | token = app.create_auth_token(gui_env) | 428 | token = app.create_auth_token(gui_env) |
137 | @@ -435,7 +437,7 @@ | |||
138 | 435 | 'Run "juju quickstart -i" if you want to manage\n' | 437 | 'Run "juju quickstart -i" if you want to manage\n' |
139 | 436 | 'or bootstrap your Juju environments using the\n' | 438 | 'or bootstrap your Juju environments using the\n' |
140 | 437 | 'interactive session.\n' | 439 | 'interactive session.\n' |
142 | 438 | 'Run "juju destroy-environment -e {env_name} [-y]"\n' | 440 | 'Run "{sudo}juju destroy-environment -e {env_name} [-y]"\n' |
143 | 439 | 'to destroy the environment you just bootstrapped.'.format( | 441 | 'to destroy the environment you just bootstrapped.'.format( |
145 | 440 | env_name=options.env_name) | 442 | env_name=options.env_name, sudo='sudo ' if is_local else '') |
146 | 441 | ) | 443 | ) |
147 | 442 | 444 | ||
148 | === modified file 'quickstart/tests/test_app.py' | |||
149 | --- quickstart/tests/test_app.py 2014-01-08 21:25:36 +0000 | |||
150 | +++ quickstart/tests/test_app.py 2014-01-17 18:31:35 +0000 | |||
151 | @@ -1009,34 +1009,50 @@ | |||
152 | 1009 | 1009 | ||
153 | 1010 | 1010 | ||
154 | 1011 | @helpers.mock_print | 1011 | @helpers.mock_print |
156 | 1012 | class TestWatch(ProgramExitTestsMixin, unittest.TestCase): | 1012 | class TestWatch( |
157 | 1013 | ProgramExitTestsMixin, helpers.ValueErrorTestsMixin, | ||
158 | 1014 | unittest.TestCase): | ||
159 | 1013 | 1015 | ||
160 | 1014 | address = 'unit.example.com' | 1016 | address = 'unit.example.com' |
162 | 1015 | change_pending = ('unit', 'change', { | 1017 | change_machine_pending = ('change', { |
163 | 1018 | 'Id': '0', | ||
164 | 1019 | 'Status': 'pending', | ||
165 | 1020 | }) | ||
166 | 1021 | change_machine_started = ('change', { | ||
167 | 1022 | 'Id': '0', | ||
168 | 1023 | 'Status': 'started', | ||
169 | 1024 | }) | ||
170 | 1025 | change_unit_pending = ('change', { | ||
171 | 1026 | 'MachineId': '0', | ||
172 | 1016 | 'Name': 'django/42', | 1027 | 'Name': 'django/42', |
173 | 1017 | 'PublicAddress': '', | 1028 | 'PublicAddress': '', |
174 | 1018 | 'Status': 'pending', | 1029 | 'Status': 'pending', |
175 | 1019 | }) | 1030 | }) |
177 | 1020 | change_reachable = ('unit', 'change', { | 1031 | change_unit_reachable = ('change', { |
178 | 1032 | 'MachineId': '0', | ||
179 | 1021 | 'Name': 'django/42', | 1033 | 'Name': 'django/42', |
180 | 1022 | 'PublicAddress': address, | 1034 | 'PublicAddress': address, |
181 | 1023 | 'Status': 'pending', | 1035 | 'Status': 'pending', |
182 | 1024 | }) | 1036 | }) |
184 | 1025 | change_installed = ('unit', 'change', { | 1037 | change_unit_installed = ('change', { |
185 | 1038 | 'MachineId': '0', | ||
186 | 1026 | 'Name': 'django/42', | 1039 | 'Name': 'django/42', |
187 | 1027 | 'PublicAddress': address, | 1040 | 'PublicAddress': address, |
188 | 1028 | 'Status': 'installed', | 1041 | 'Status': 'installed', |
189 | 1029 | }) | 1042 | }) |
191 | 1030 | change_started = ('unit', 'change', { | 1043 | change_unit_started = ('change', { |
192 | 1031 | 'MachineId': '0', | 1044 | 'MachineId': '0', |
193 | 1032 | 'Name': 'django/42', | 1045 | 'Name': 'django/42', |
194 | 1033 | 'PublicAddress': address, | 1046 | 'PublicAddress': address, |
195 | 1034 | 'Status': 'started', | 1047 | 'Status': 'started', |
196 | 1035 | }) | 1048 | }) |
201 | 1036 | pending_call = mock.call('django/42 deployment is pending') | 1049 | machine_pending_call = mock.call('machine 0 provisioning is pending') |
202 | 1037 | reachable_call = mock.call('setting up django/42 on {}'.format(address)) | 1050 | machine_started_call = mock.call('machine 0 is started') |
203 | 1038 | installed_call = mock.call('django/42 is installed') | 1051 | unit_pending_call = mock.call('django/42 deployment is pending') |
204 | 1039 | started_call = mock.call('django/42 is ready on machine 0') | 1052 | unit_reachable_call = mock.call( |
205 | 1053 | 'setting up django/42 on {}'.format(address)) | ||
206 | 1054 | unit_installed_call = mock.call('django/42 is installed') | ||
207 | 1055 | unit_started_call = mock.call('django/42 is ready on machine 0') | ||
208 | 1040 | 1056 | ||
209 | 1041 | def make_env(self, changes): | 1057 | def make_env(self, changes): |
210 | 1042 | """Create and return a patched Environment instance. | 1058 | """Create and return a patched Environment instance. |
211 | @@ -1048,131 +1064,220 @@ | |||
212 | 1048 | env.watch_changes().next.side_effect = changes | 1064 | env.watch_changes().next.side_effect = changes |
213 | 1049 | return env | 1065 | return env |
214 | 1050 | 1066 | ||
215 | 1067 | # The following group of tests exercises both the function return value and | ||
216 | 1068 | # the function output, even if the output is handled by sub-functions. | ||
217 | 1069 | # This is done to simulate the different user experiences of observing the | ||
218 | 1070 | # environment evolution while the unit is deploying. | ||
219 | 1071 | |||
220 | 1051 | def test_unit_life(self, mock_print): | 1072 | def test_unit_life(self, mock_print): |
221 | 1052 | # The glorious moments in the unit's life are properly highlighted. | 1073 | # The glorious moments in the unit's life are properly highlighted. |
222 | 1074 | # The machine achievements are also celebrated. | ||
223 | 1053 | env = self.make_env([ | 1075 | env = self.make_env([ |
228 | 1054 | self.change_pending, | 1076 | ([self.change_unit_pending], [self.change_machine_pending]), |
229 | 1055 | self.change_reachable, | 1077 | ([], [self.change_machine_started]), |
230 | 1056 | self.change_installed, | 1078 | ([self.change_unit_reachable], []), |
231 | 1057 | self.change_started, | 1079 | ([self.change_unit_installed], []), |
232 | 1080 | ([self.change_unit_started], []), | ||
233 | 1058 | ]) | 1081 | ]) |
234 | 1059 | address = app.watch(env, 'django/42') | 1082 | address = app.watch(env, 'django/42') |
235 | 1060 | self.assertEqual(self.address, address) | 1083 | self.assertEqual(self.address, address) |
236 | 1084 | self.assertEqual(6, mock_print.call_count) | ||
237 | 1061 | mock_print.assert_has_calls([ | 1085 | mock_print.assert_has_calls([ |
242 | 1062 | self.pending_call, | 1086 | self.unit_pending_call, |
243 | 1063 | self.reachable_call, | 1087 | self.machine_pending_call, |
244 | 1064 | self.installed_call, | 1088 | self.machine_started_call, |
245 | 1065 | self.started_call, | 1089 | self.unit_reachable_call, |
246 | 1090 | self.unit_installed_call, | ||
247 | 1091 | self.unit_started_call, | ||
248 | 1066 | ]) | 1092 | ]) |
249 | 1067 | 1093 | ||
250 | 1068 | def test_weird_order(self, mock_print): | 1094 | def test_weird_order(self, mock_print): |
251 | 1069 | # Strange unit evolutions are handled. | 1095 | # Strange unit evolutions are handled. |
252 | 1070 | env = self.make_env([ | 1096 | env = self.make_env([ |
257 | 1071 | self.change_reachable, | 1097 | # The unit is first reachable and then pending. The machine starts |
258 | 1072 | self.change_pending, | 1098 | # when the unit is already installed. All of this makes no sense |
259 | 1073 | self.change_installed, | 1099 | # and should never happen, but if it does, we deal with it. |
260 | 1074 | self.change_started, | 1100 | ([self.change_unit_reachable], []), |
261 | 1101 | ([self.change_unit_pending], [self.change_machine_pending]), | ||
262 | 1102 | ([self.change_unit_installed], []), | ||
263 | 1103 | ([], [self.change_machine_started]), | ||
264 | 1104 | ([self.change_unit_started], []), | ||
265 | 1075 | ]) | 1105 | ]) |
266 | 1076 | address = app.watch(env, 'django/42') | 1106 | address = app.watch(env, 'django/42') |
267 | 1077 | self.assertEqual(self.address, address) | 1107 | self.assertEqual(self.address, address) |
268 | 1108 | self.assertEqual(6, mock_print.call_count) | ||
269 | 1078 | mock_print.assert_has_calls([ | 1109 | mock_print.assert_has_calls([ |
274 | 1079 | self.reachable_call, | 1110 | self.unit_reachable_call, |
275 | 1080 | self.pending_call, | 1111 | self.unit_pending_call, |
276 | 1081 | self.installed_call, | 1112 | self.machine_pending_call, |
277 | 1082 | self.started_call, | 1113 | self.unit_installed_call, |
278 | 1114 | self.machine_started_call, | ||
279 | 1115 | self.unit_started_call, | ||
280 | 1083 | ]) | 1116 | ]) |
281 | 1084 | 1117 | ||
282 | 1085 | def test_missing_changes(self, mock_print): | 1118 | def test_missing_changes(self, mock_print): |
290 | 1086 | # Only the started change is strictly required. | 1119 | # Only the unit started change is strictly required. |
291 | 1087 | env = self.make_env([self.change_started]) | 1120 | env = self.make_env([([self.change_unit_started], [])]) |
292 | 1088 | address = app.watch(env, 'django/42') | 1121 | address = app.watch(env, 'django/42') |
293 | 1089 | self.assertEqual(self.address, address) | 1122 | self.assertEqual(self.address, address) |
294 | 1090 | mock_print.assert_has_calls([ | 1123 | self.assertEqual(2, mock_print.call_count) |
295 | 1091 | self.reachable_call, | 1124 | mock_print.assert_has_calls([ |
296 | 1092 | self.started_call, | 1125 | self.unit_reachable_call, |
297 | 1126 | self.unit_started_call, | ||
298 | 1127 | ]) | ||
299 | 1128 | |||
300 | 1129 | def test_ignored_machine_changes(self, mock_print): | ||
301 | 1130 | # All machine changes are ignored until the application knows what | ||
302 | 1131 | # machine the unit belongs to. | ||
303 | 1132 | env = self.make_env([ | ||
304 | 1133 | ([], [self.change_machine_pending]), | ||
305 | 1134 | ([], [self.change_machine_started]), | ||
306 | 1135 | ([self.change_unit_started], []), | ||
307 | 1136 | ]) | ||
308 | 1137 | address = app.watch(env, 'django/42') | ||
309 | 1138 | self.assertEqual(self.address, address) | ||
310 | 1139 | # No machine related messages have been printed. | ||
311 | 1140 | self.assertEqual(2, mock_print.call_count) | ||
312 | 1141 | mock_print.assert_has_calls([ | ||
313 | 1142 | self.unit_reachable_call, | ||
314 | 1143 | self.unit_started_call, | ||
315 | 1144 | ]) | ||
316 | 1145 | |||
317 | 1146 | def test_unit_already_deployed(self, mock_print): | ||
318 | 1147 | # Simulate the unit we are observing has been already deployed. | ||
319 | 1148 | # This happens, e.g., when executing Quickstart a second time, and both | ||
320 | 1149 | # the unit and the machine are already started. | ||
321 | 1150 | env = self.make_env([ | ||
322 | 1151 | ([self.change_unit_started], [self.change_machine_started]), | ||
323 | 1152 | ]) | ||
324 | 1153 | address = app.watch(env, 'django/42') | ||
325 | 1154 | self.assertEqual(self.address, address) | ||
326 | 1155 | self.assertEqual(2, mock_print.call_count) | ||
327 | 1156 | |||
328 | 1157 | def test_machine_already_started(self, mock_print): | ||
329 | 1158 | # Simulate the unit is being deployed on an already started machine. | ||
330 | 1159 | # This happens, e.g., when running Quickstart on a non-local | ||
331 | 1160 | # environment type: the unit is deployed on the bootstrap node, which | ||
332 | 1161 | # is assumed to be started. | ||
333 | 1162 | env = self.make_env([ | ||
334 | 1163 | ([self.change_unit_pending], [self.change_machine_started]), | ||
335 | 1164 | ([self.change_unit_reachable], []), | ||
336 | 1165 | ([self.change_unit_installed], []), | ||
337 | 1166 | ([self.change_unit_started], []), | ||
338 | 1167 | ]) | ||
339 | 1168 | address = app.watch(env, 'django/42') | ||
340 | 1169 | self.assertEqual(self.address, address) | ||
341 | 1170 | self.assertEqual(5, mock_print.call_count) | ||
342 | 1171 | mock_print.assert_has_calls([ | ||
343 | 1172 | self.unit_pending_call, | ||
344 | 1173 | self.machine_started_call, | ||
345 | 1174 | self.unit_reachable_call, | ||
346 | 1175 | self.unit_installed_call, | ||
347 | 1176 | self.unit_started_call, | ||
348 | 1093 | ]) | 1177 | ]) |
349 | 1094 | 1178 | ||
350 | 1095 | def test_extraneous_changes(self, mock_print): | 1179 | def test_extraneous_changes(self, mock_print): |
352 | 1096 | # Extraneous changes are properly handled. | 1180 | # Changes to units or machines we are not observing are ignored. Also |
353 | 1181 | # ensure that repeated changes to a single entity are ignored, even if | ||
354 | 1182 | # they are unlikely to happen. | ||
355 | 1183 | change_another_machine_pending = ('change', { | ||
356 | 1184 | 'Id': '42', | ||
357 | 1185 | 'Status': 'pending', | ||
358 | 1186 | }) | ||
359 | 1187 | change_another_machine_started = ('change', { | ||
360 | 1188 | 'Id': '1', | ||
361 | 1189 | 'Status': 'started', | ||
362 | 1190 | }) | ||
363 | 1191 | change_another_unit_pending = ('change', { | ||
364 | 1192 | 'MachineId': '0', | ||
365 | 1193 | 'Name': 'haproxy/0', | ||
366 | 1194 | 'Status': 'pending', | ||
367 | 1195 | }) | ||
368 | 1196 | change_another_unit_started = ('change', { | ||
369 | 1197 | 'MachineId': '0', | ||
370 | 1198 | 'Name': 'haproxy/0', | ||
371 | 1199 | 'Status': 'started', | ||
372 | 1200 | }) | ||
373 | 1097 | env = self.make_env([ | 1201 | env = self.make_env([ |
391 | 1098 | self.change_pending, | 1202 | # Add a repeated change. |
392 | 1099 | ('unit', 'change', { | 1203 | ([self.change_unit_pending, self.change_unit_pending], |
393 | 1100 | 'Name': 'django/42', | 1204 | [self.change_machine_pending]), |
394 | 1101 | 'Status': 'pending', | 1205 | # Add extraneous unit and machine changes. |
395 | 1102 | 'PublicAddress': '', | 1206 | ([change_another_unit_pending], [change_another_machine_pending]), |
396 | 1103 | 'Series': 'precise', | 1207 | # Add a change to an extraneous machine. |
397 | 1104 | }), | 1208 | ([], [change_another_machine_started, |
398 | 1105 | self.change_reachable, | 1209 | self.change_machine_started]), |
399 | 1106 | self.change_installed, | 1210 | # Add a change to an extraneous unit. |
400 | 1107 | ('unit', 'change', { | 1211 | ([change_another_unit_started, self.change_unit_reachable], []), |
401 | 1108 | 'Name': 'django/42', | 1212 | ([self.change_unit_installed], []), |
402 | 1109 | 'Status': 'installed', | 1213 | # Add another repeated change. |
403 | 1110 | 'PublicAddress': self.address, | 1214 | ([self.change_unit_started, self.change_unit_started], []), |
387 | 1111 | 'Series': 'precise', | ||
388 | 1112 | 'MachineId': '3', | ||
389 | 1113 | }), | ||
390 | 1114 | self.change_started, | ||
404 | 1115 | ]) | 1215 | ]) |
405 | 1116 | address = app.watch(env, 'django/42') | 1216 | address = app.watch(env, 'django/42') |
406 | 1117 | self.assertEqual(self.address, address) | 1217 | self.assertEqual(self.address, address) |
407 | 1218 | self.assertEqual(6, mock_print.call_count) | ||
408 | 1118 | mock_print.assert_has_calls([ | 1219 | mock_print.assert_has_calls([ |
413 | 1119 | self.pending_call, | 1220 | self.unit_pending_call, |
414 | 1120 | self.reachable_call, | 1221 | self.machine_pending_call, |
415 | 1121 | self.installed_call, | 1222 | self.machine_started_call, |
416 | 1122 | self.started_call, | 1223 | self.unit_reachable_call, |
417 | 1224 | self.unit_installed_call, | ||
418 | 1225 | self.unit_started_call, | ||
419 | 1123 | ]) | 1226 | ]) |
420 | 1124 | 1227 | ||
421 | 1125 | def test_api_error(self, mock_print): | 1228 | def test_api_error(self, mock_print): |
422 | 1126 | # A ProgramExit is raised if an error occurs in one of the API calls. | 1229 | # A ProgramExit is raised if an error occurs in one of the API calls. |
423 | 1127 | env = self.make_env([ | 1230 | env = self.make_env([ |
425 | 1128 | self.change_pending, | 1231 | ([self.change_unit_pending], []), |
426 | 1129 | self.make_env_error('next returned an error'), | 1232 | self.make_env_error('next returned an error'), |
427 | 1130 | ]) | 1233 | ]) |
428 | 1131 | expected = 'bad API server response: next returned an error' | 1234 | expected = 'bad API server response: next returned an error' |
429 | 1132 | with self.assert_program_exit(expected): | 1235 | with self.assert_program_exit(expected): |
430 | 1133 | app.watch(env, 'django/42') | 1236 | app.watch(env, 'django/42') |
432 | 1134 | mock_print.assert_has_calls([self.pending_call]) | 1237 | self.assertEqual(1, mock_print.call_count) |
433 | 1238 | mock_print.assert_has_calls([self.unit_pending_call]) | ||
434 | 1135 | 1239 | ||
435 | 1136 | def test_other_errors(self, mock_print): | 1240 | def test_other_errors(self, mock_print): |
436 | 1137 | # Any other errors occurred during the process are not trapped. | 1241 | # Any other errors occurred during the process are not trapped. |
437 | 1138 | error = ValueError('explode!') | 1242 | error = ValueError('explode!') |
440 | 1139 | env = self.make_env([self.change_installed, error]) | 1243 | env = self.make_env([([self.change_unit_installed], []), error]) |
441 | 1140 | with self.assertRaises(ValueError) as context_manager: | 1244 | with self.assert_value_error('explode!'): |
442 | 1141 | app.watch(env, 'django/42') | 1245 | app.watch(env, 'django/42') |
445 | 1142 | mock_print.assert_has_calls([self.reachable_call, self.installed_call]) | 1246 | self.assertEqual(2, mock_print.call_count) |
446 | 1143 | self.assertIs(error, context_manager.exception) | 1247 | mock_print.assert_has_calls([ |
447 | 1248 | self.unit_reachable_call, self.unit_installed_call]) | ||
448 | 1144 | 1249 | ||
459 | 1145 | def test_unit_removed(self, mock_print): | 1250 | def test_machine_status_error(self, mock_print): |
460 | 1146 | # A ProgramExit is raised if Juju removed the unit. | 1251 | # A ProgramExit is raised if an the machine is found in an error state. |
461 | 1147 | env = self.make_env([ | 1252 | change_machine_error = ('change', { |
462 | 1148 | self.change_pending, | 1253 | 'Id': '0', |
463 | 1149 | ('unit', 'remove', { | 1254 | 'Status': 'error', |
464 | 1150 | 'Name': 'django/42', | 1255 | 'StatusInfo': 'oddities', |
465 | 1151 | 'Status': 'pending', | 1256 | }) |
466 | 1152 | 'PublicAddress': '', | 1257 | # The unit pending change is required to make the function know which |
467 | 1153 | }), | 1258 | # machine to observe. |
468 | 1154 | self.change_reachable, | 1259 | env = self.make_env([( |
469 | 1260 | [self.change_unit_pending], [change_machine_error]), | ||
470 | 1155 | ]) | 1261 | ]) |
472 | 1156 | with self.assert_program_exit('django/42 unexpectedly removed'): | 1262 | expected = 'machine 0 is in an error state: error: oddities' |
473 | 1263 | with self.assert_program_exit(expected): | ||
474 | 1157 | app.watch(env, 'django/42') | 1264 | app.watch(env, 'django/42') |
476 | 1158 | mock_print.assert_has_calls([self.pending_call]) | 1265 | self.assertEqual(1, mock_print.call_count) |
477 | 1266 | mock_print.assert_has_calls([self.unit_pending_call]) | ||
478 | 1159 | 1267 | ||
480 | 1160 | def test_status_error(self, mock_print): | 1268 | def test_unit_status_error(self, mock_print): |
481 | 1161 | # A ProgramExit is raised if an the unit is found in an error state. | 1269 | # A ProgramExit is raised if an the unit is found in an error state. |
492 | 1162 | env = self.make_env([ | 1270 | change_unit_error = ('change', { |
493 | 1163 | self.change_pending, | 1271 | 'MachineId': '0', |
494 | 1164 | ('unit', 'change', { | 1272 | 'Name': 'django/42', |
495 | 1165 | 'Name': 'django/42', | 1273 | 'Status': 'error', |
496 | 1166 | 'Status': 'error', | 1274 | 'StatusInfo': 'install failure', |
497 | 1167 | 'StatusInfo': 'install failure', | 1275 | }) |
498 | 1168 | 'PublicAddress': '', | 1276 | env = self.make_env([([change_unit_error], [])]) |
489 | 1169 | }), | ||
490 | 1170 | self.change_reachable, | ||
491 | 1171 | ]) | ||
499 | 1172 | expected = 'django/42 is in an error state: error: install failure' | 1277 | expected = 'django/42 is in an error state: error: install failure' |
500 | 1173 | with self.assert_program_exit(expected): | 1278 | with self.assert_program_exit(expected): |
501 | 1174 | app.watch(env, 'django/42') | 1279 | app.watch(env, 'django/42') |
503 | 1175 | mock_print.assert_has_calls([self.pending_call]) | 1280 | self.assertFalse(mock_print.called) |
504 | 1176 | 1281 | ||
505 | 1177 | 1282 | ||
506 | 1178 | class TestDeployBundle(ProgramExitTestsMixin, unittest.TestCase): | 1283 | class TestDeployBundle(ProgramExitTestsMixin, unittest.TestCase): |
507 | 1179 | 1284 | ||
508 | === modified file 'quickstart/tests/test_utils.py' | |||
509 | --- quickstart/tests/test_utils.py 2014-01-13 16:51:35 +0000 | |||
510 | +++ quickstart/tests/test_utils.py 2014-01-17 18:31:35 +0000 | |||
511 | @@ -620,54 +620,6 @@ | |||
512 | 620 | self.assertEqual(list.append.__doc__, self.func.__doc__) | 620 | self.assertEqual(list.append.__doc__, self.func.__doc__) |
513 | 621 | 621 | ||
514 | 622 | 622 | ||
515 | 623 | class TestUnitChanges(unittest.TestCase): | ||
516 | 624 | |||
517 | 625 | unit = 'django/42' | ||
518 | 626 | |||
519 | 627 | def test_change_found(self): | ||
520 | 628 | # A relevant change is correctly found and returned. | ||
521 | 629 | change = ('unit', 'change', {'Name': self.unit, 'Status': 'pending'}) | ||
522 | 630 | self.assertEqual(change, utils.unit_changes(self.unit, [change])) | ||
523 | 631 | |||
524 | 632 | def test_change_found_removed(self): | ||
525 | 633 | # A unit removal is considered a relevant change. | ||
526 | 634 | change = ('unit', 'remove', {'Name': self.unit, 'Status': 'started'}) | ||
527 | 635 | self.assertEqual(change, utils.unit_changes(self.unit, [change])) | ||
528 | 636 | |||
529 | 637 | def test_not_a_unit(self): | ||
530 | 638 | # Changes to other entities are ignored. | ||
531 | 639 | change = ('service', 'change', {'Name': 'django', 'Exposed': True}) | ||
532 | 640 | self.assertIsNone(utils.unit_changes(self.unit, [change])) | ||
533 | 641 | |||
534 | 642 | def test_not_a_specific_unit(self): | ||
535 | 643 | # Changes to other units are ignored. | ||
536 | 644 | change = ('unit', 'change', {'Name': 'django/0', 'Status': 'pending'}) | ||
537 | 645 | self.assertIsNone(utils.unit_changes(self.unit, [change])) | ||
538 | 646 | |||
539 | 647 | def test_multiple_changes(self): | ||
540 | 648 | # A relevant change is found between multiple ones. | ||
541 | 649 | change = ('unit', 'change', {'Name': self.unit, 'Status': 'pending'}) | ||
542 | 650 | changeset = [ | ||
543 | 651 | ('machine', 'change', {'Id': '0', 'Status': 'started'}), | ||
544 | 652 | change, | ||
545 | 653 | ('service', 'change', {'Name': 'django', 'Exposed': True}), | ||
546 | 654 | ] | ||
547 | 655 | self.assertEqual(change, utils.unit_changes(self.unit, changeset)) | ||
548 | 656 | |||
549 | 657 | def test_multiple_unit_changes(self): | ||
550 | 658 | # A changeset is not supposed to include multiple change for a single | ||
551 | 659 | # unit. The function just picks the first change. | ||
552 | 660 | data = {'Name': self.unit, 'Status': 'pending'} | ||
553 | 661 | changeset = [ | ||
554 | 662 | ('unit', 'change', data), | ||
555 | 663 | ('unit', 'remove', data), | ||
556 | 664 | ('unit', 'exterminated', data), | ||
557 | 665 | ] | ||
558 | 666 | self.assertEqual( | ||
559 | 667 | ('unit', 'change', data), | ||
560 | 668 | utils.unit_changes(self.unit, changeset)) | ||
561 | 669 | |||
562 | 670 | |||
563 | 671 | class TestUrlread(unittest.TestCase): | 623 | class TestUrlread(unittest.TestCase): |
564 | 672 | 624 | ||
565 | 673 | def patch_urlopen(self, contents=None, error=None, content_type=None): | 625 | def patch_urlopen(self, contents=None, error=None, content_type=None): |
566 | 674 | 626 | ||
567 | === added file 'quickstart/tests/test_watchers.py' | |||
568 | --- quickstart/tests/test_watchers.py 1970-01-01 00:00:00 +0000 | |||
569 | +++ quickstart/tests/test_watchers.py 2014-01-17 18:31:35 +0000 | |||
570 | @@ -0,0 +1,266 @@ | |||
571 | 1 | # This file is part of the Juju Quickstart Plugin, which lets users set up a | ||
572 | 2 | # Juju environment in very few steps (https://launchpad.net/juju-quickstart). | ||
573 | 3 | # Copyright (C) 2014 Canonical Ltd. | ||
574 | 4 | # | ||
575 | 5 | # This program is free software: you can redistribute it and/or modify it under | ||
576 | 6 | # the terms of the GNU Affero General Public License version 3, as published by | ||
577 | 7 | # the Free Software Foundation. | ||
578 | 8 | # | ||
579 | 9 | # This program is distributed in the hope that it will be useful, but WITHOUT | ||
580 | 10 | # ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, | ||
581 | 11 | # SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | ||
582 | 12 | # Affero General Public License for more details. | ||
583 | 13 | # | ||
584 | 14 | # You should have received a copy of the GNU Affero General Public License | ||
585 | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
586 | 16 | |||
587 | 17 | """Tests for the Juju Quickstart environments watching utilities.""" | ||
588 | 18 | |||
589 | 19 | from __future__ import unicode_literals | ||
590 | 20 | |||
591 | 21 | import unittest | ||
592 | 22 | |||
593 | 23 | import mock | ||
594 | 24 | |||
595 | 25 | from quickstart import watchers | ||
596 | 26 | from quickstart.tests import helpers | ||
597 | 27 | |||
598 | 28 | |||
599 | 29 | class TestParseMachineChange(helpers.ValueErrorTestsMixin, unittest.TestCase): | ||
600 | 30 | |||
601 | 31 | def test_machine_removed(self): | ||
602 | 32 | # A ValueError is raised if the change represents a machine removal. | ||
603 | 33 | data = {'Id': '1', 'Status': 'started'} | ||
604 | 34 | with self.assert_value_error('machine 1 unexpectedly removed'): | ||
605 | 35 | watchers.parse_machine_change('remove', data, '') | ||
606 | 36 | |||
607 | 37 | def test_machine_error(self): | ||
608 | 38 | # A ValueError is raised if the machine is in an error state. | ||
609 | 39 | data = {'Id': '1', 'Status': 'error', 'StatusInfo': 'bad wolf'} | ||
610 | 40 | expected_error = 'machine 1 is in an error state: error: bad wolf' | ||
611 | 41 | with self.assert_value_error(expected_error): | ||
612 | 42 | watchers.parse_machine_change('change', data, '') | ||
613 | 43 | |||
614 | 44 | @helpers.mock_print | ||
615 | 45 | def test_pending_status_notified(self, mock_print): | ||
616 | 46 | # A message is printed to stdout when the machine changes its status | ||
617 | 47 | # to "pending". The new status is also returned by the function. | ||
618 | 48 | data = {'Id': '1', 'Status': 'pending'} | ||
619 | 49 | status = watchers.parse_machine_change('change', data, '') | ||
620 | 50 | self.assertEqual('pending', status) | ||
621 | 51 | mock_print.assert_called_once_with('machine 1 provisioning is pending') | ||
622 | 52 | |||
623 | 53 | @helpers.mock_print | ||
624 | 54 | def test_started_status_notified(self, mock_print): | ||
625 | 55 | # A message is printed to stdout when the machine changes its status | ||
626 | 56 | # to "started". The new status is also returned by the function. | ||
627 | 57 | data = {'Id': '42', 'Status': 'started'} | ||
628 | 58 | status = watchers.parse_machine_change('change', data, 'pending') | ||
629 | 59 | self.assertEqual('started', status) | ||
630 | 60 | mock_print.assert_called_once_with('machine 42 is started') | ||
631 | 61 | |||
632 | 62 | @helpers.mock_print | ||
633 | 63 | def test_status_not_changed(self, mock_print): | ||
634 | 64 | # If the status in the machine change and the given current status are | ||
635 | 65 | # the same value, nothing is printed and the status is returned. | ||
636 | 66 | data = {'Id': '47', 'Status': 'pending'} | ||
637 | 67 | status = watchers.parse_machine_change('change', data, 'pending') | ||
638 | 68 | self.assertEqual('pending', status) | ||
639 | 69 | self.assertFalse(mock_print.called) | ||
640 | 70 | |||
641 | 71 | |||
642 | 72 | class TestParseUnitChange(helpers.ValueErrorTestsMixin, unittest.TestCase): | ||
643 | 73 | |||
644 | 74 | def test_unit_removed(self): | ||
645 | 75 | # A ValueError is raised if the change represents a unit removal. | ||
646 | 76 | data = {'Name': 'django/42', 'Status': 'started'} | ||
647 | 77 | with self.assert_value_error('django/42 unexpectedly removed'): | ||
648 | 78 | # The last two arguments are the current status and address. | ||
649 | 79 | watchers.parse_unit_change('remove', data, '', '') | ||
650 | 80 | |||
651 | 81 | def test_unit_error(self): | ||
652 | 82 | # A ValueError is raised if the unit is in an error state. | ||
653 | 83 | data = { | ||
654 | 84 | 'Name': 'django/0', | ||
655 | 85 | 'Status': 'start error', | ||
656 | 86 | 'StatusInfo': 'bad wolf', | ||
657 | 87 | } | ||
658 | 88 | expected_error = 'django/0 is in an error state: start error: bad wolf' | ||
659 | 89 | with self.assert_value_error(expected_error): | ||
660 | 90 | # The last two arguments are the current status and address. | ||
661 | 91 | watchers.parse_unit_change('change', data, '', '') | ||
662 | 92 | |||
663 | 93 | @helpers.mock_print | ||
664 | 94 | def test_address_notified(self, mock_print): | ||
665 | 95 | # A message is printed to stdout when the unit obtains a public | ||
666 | 96 | # address. The function returns the status, the new address and the | ||
667 | 97 | # machine identifier. | ||
668 | 98 | data = { | ||
669 | 99 | 'Name': 'haproxy/2', | ||
670 | 100 | 'Status': 'pending', | ||
671 | 101 | 'PublicAddress': 'haproxy2.example.com', | ||
672 | 102 | 'MachineId': '42', | ||
673 | 103 | } | ||
674 | 104 | status, address, machine_id = watchers.parse_unit_change( | ||
675 | 105 | 'change', data, 'pending', '') | ||
676 | 106 | self.assertEqual('pending', status) | ||
677 | 107 | self.assertEqual('haproxy2.example.com', address) | ||
678 | 108 | self.assertEqual('42', machine_id) | ||
679 | 109 | mock_print.assert_called_once_with( | ||
680 | 110 | 'setting up haproxy/2 on haproxy2.example.com') | ||
681 | 111 | |||
682 | 112 | @helpers.mock_print | ||
683 | 113 | def test_pending_status_notified(self, mock_print): | ||
684 | 114 | # A message is printed to stdout when the unit changes its status to | ||
685 | 115 | # "pending". The function returns the new status, the address and the | ||
686 | 116 | # machine identifier. The last two values are empty strings if the unit | ||
687 | 117 | # has not yet been assigned to a machine. | ||
688 | 118 | data = {'Name': 'django/1', 'Status': 'pending', 'PublicAddress': ''} | ||
689 | 119 | # The last two arguments are the current status and address. | ||
690 | 120 | status, address, machine_id = watchers.parse_unit_change( | ||
691 | 121 | 'change', data, '', '') | ||
692 | 122 | self.assertEqual('pending', status) | ||
693 | 123 | self.assertEqual('', address) | ||
694 | 124 | self.assertEqual('', machine_id) | ||
695 | 125 | mock_print.assert_called_once_with('django/1 deployment is pending') | ||
696 | 126 | |||
697 | 127 | @helpers.mock_print | ||
698 | 128 | def test_installed_status_notified(self, mock_print): | ||
699 | 129 | # A message is printed to stdout when the unit changes its status to | ||
700 | 130 | # "installed". The function returns the new status, the address and the | ||
701 | 131 | # machine identifier. | ||
702 | 132 | data = { | ||
703 | 133 | 'Name': 'django/42', | ||
704 | 134 | 'Status': 'installed', | ||
705 | 135 | 'PublicAddress': 'django42.example.com', | ||
706 | 136 | 'MachineId': '1', | ||
707 | 137 | } | ||
708 | 138 | status, address, machine_id = watchers.parse_unit_change( | ||
709 | 139 | 'change', data, 'pending', 'django42.example.com') | ||
710 | 140 | self.assertEqual('installed', status) | ||
711 | 141 | self.assertEqual('django42.example.com', address) | ||
712 | 142 | self.assertEqual('1', machine_id) | ||
713 | 143 | mock_print.assert_called_once_with('django/42 is installed') | ||
714 | 144 | |||
715 | 145 | @helpers.mock_print | ||
716 | 146 | def test_started_status_notified(self, mock_print): | ||
717 | 147 | # A message is printed to stdout when the unit changes its status to | ||
718 | 148 | # "started". The function returns the new status, the address and the | ||
719 | 149 | # machine identifier. | ||
720 | 150 | data = { | ||
721 | 151 | 'Name': 'wordpress/0', | ||
722 | 152 | 'Status': 'started', | ||
723 | 153 | 'PublicAddress': 'wordpress0.example.com', | ||
724 | 154 | 'MachineId': '0', | ||
725 | 155 | } | ||
726 | 156 | status, address, machine_id = watchers.parse_unit_change( | ||
727 | 157 | 'change', data, '', 'wordpress0.example.com') | ||
728 | 158 | self.assertEqual('started', status) | ||
729 | 159 | self.assertEqual('wordpress0.example.com', address) | ||
730 | 160 | self.assertEqual('0', machine_id) | ||
731 | 161 | mock_print.assert_called_once_with('wordpress/0 is ready on machine 0') | ||
732 | 162 | |||
733 | 163 | @helpers.mock_print | ||
734 | 164 | def test_both_status_and_address_notified(self, mock_print): | ||
735 | 165 | # Both status and public address changes are notified if required. | ||
736 | 166 | data = { | ||
737 | 167 | 'Name': 'django/0', | ||
738 | 168 | 'Status': 'started', | ||
739 | 169 | 'PublicAddress': 'django42.example.com', | ||
740 | 170 | 'MachineId': '0', | ||
741 | 171 | } | ||
742 | 172 | # The last two arguments are the current status and address. | ||
743 | 173 | watchers.parse_unit_change('change', data, '', '') | ||
744 | 174 | self.assertEqual(2, mock_print.call_count) | ||
745 | 175 | mock_print.assert_has_calls([ | ||
746 | 176 | mock.call('setting up django/0 on django42.example.com'), | ||
747 | 177 | mock.call('django/0 is ready on machine 0'), | ||
748 | 178 | ]) | ||
749 | 179 | |||
750 | 180 | @helpers.mock_print | ||
751 | 181 | def test_status_not_changed(self, mock_print): | ||
752 | 182 | # If the status in the unit change and the given current status are the | ||
753 | 183 | # same value, nothing is printed and the current values are returned. | ||
754 | 184 | data = {'Name': 'django/1', 'Status': 'pending', 'PublicAddress': ''} | ||
755 | 185 | status, address, machine_id = watchers.parse_unit_change( | ||
756 | 186 | 'change', data, 'pending', '') | ||
757 | 187 | self.assertEqual('pending', status) | ||
758 | 188 | self.assertEqual('', address) | ||
759 | 189 | self.assertEqual('', machine_id) | ||
760 | 190 | self.assertFalse(mock_print.called) | ||
761 | 191 | |||
762 | 192 | |||
763 | 193 | class TestUnitMachineChanges(unittest.TestCase): | ||
764 | 194 | |||
765 | 195 | def test_unit_changes_found(self): | ||
766 | 196 | # Unit changes are correctly found and returned. | ||
767 | 197 | data1 = {'Name': 'django/42', 'Status': 'started'} | ||
768 | 198 | data2 = {'Name': 'django/47', 'Status': 'pending'} | ||
769 | 199 | changeset = [('unit', 'change', data1), ('unit', 'remove', data2)] | ||
770 | 200 | expected_unit_changes = [('change', data1), ('remove', data2)] | ||
771 | 201 | self.assertEqual( | ||
772 | 202 | (expected_unit_changes, []), | ||
773 | 203 | watchers.unit_machine_changes(changeset)) | ||
774 | 204 | |||
775 | 205 | def test_machine_changes_found(self): | ||
776 | 206 | # Machine changes are correctly found and returned. | ||
777 | 207 | data1 = {'Id': '0', 'Status': 'started'} | ||
778 | 208 | data2 = {'Id': '1', 'Status': 'error'} | ||
779 | 209 | changeset = [ | ||
780 | 210 | ('machine', 'change', data1), | ||
781 | 211 | ('machine', 'remove', data2), | ||
782 | 212 | ] | ||
783 | 213 | expected_machine_changes = [('change', data1), ('remove', data2)] | ||
784 | 214 | self.assertEqual( | ||
785 | 215 | ([], expected_machine_changes), | ||
786 | 216 | watchers.unit_machine_changes(changeset)) | ||
787 | 217 | |||
788 | 218 | def test_unit_and_machine_changes_found(self): | ||
789 | 219 | # Changes to unit and machines are reordered, grouped and returned. | ||
790 | 220 | machine_data1 = {'Id': '0', 'Status': 'started'} | ||
791 | 221 | machine_data2 = {'Id': '42', 'Status': 'started'} | ||
792 | 222 | unit_data1 = {'Name': 'django/42', 'Status': 'error'} | ||
793 | 223 | unit_data2 = {'Name': 'haproxy/47', 'Status': 'pending'} | ||
794 | 224 | unit_data3 = {'Name': 'wordpress/0', 'Status': 'installed'} | ||
795 | 225 | changeset = [ | ||
796 | 226 | ('machine', 'change', machine_data1), | ||
797 | 227 | ('unit', 'change', unit_data1), | ||
798 | 228 | ('machine', 'remove', machine_data2), | ||
799 | 229 | ('unit', 'change', unit_data2), | ||
800 | 230 | ('unit', 'remove', unit_data3), | ||
801 | 231 | ] | ||
802 | 232 | expected_unit_changes = [ | ||
803 | 233 | ('change', unit_data1), | ||
804 | 234 | ('change', unit_data2), | ||
805 | 235 | ('remove', unit_data3), | ||
806 | 236 | ] | ||
807 | 237 | expected_machine_changes = [ | ||
808 | 238 | ('change', machine_data1), | ||
809 | 239 | ('remove', machine_data2), | ||
810 | 240 | ] | ||
811 | 241 | self.assertEqual( | ||
812 | 242 | (expected_unit_changes, expected_machine_changes), | ||
813 | 243 | watchers.unit_machine_changes(changeset)) | ||
814 | 244 | |||
815 | 245 | def test_other_entities(self): | ||
816 | 246 | # Changes to other entities (like services) are ignored. | ||
817 | 247 | machine_data = {'Id': '0', 'Status': 'started'} | ||
818 | 248 | unit_data = {'Name': 'django/42', 'Status': 'error'} | ||
819 | 249 | changeset = [ | ||
820 | 250 | ('machine', 'change', machine_data), | ||
821 | 251 | ('service', 'change', {'Name': 'django', 'Status': 'pending'}), | ||
822 | 252 | ('unit', 'change', unit_data), | ||
823 | 253 | ('service', 'remove', {'Name': 'haproxy', 'Status': 'started'}), | ||
824 | 254 | ] | ||
825 | 255 | expected_changes = ( | ||
826 | 256 | [('change', unit_data)], | ||
827 | 257 | [('change', machine_data)], | ||
828 | 258 | ) | ||
829 | 259 | self.assertEqual( | ||
830 | 260 | expected_changes, watchers.unit_machine_changes(changeset)) | ||
831 | 261 | |||
832 | 262 | def test_empty_changeset(self): | ||
833 | 263 | # Two empty lists are returned if the changeset is empty. | ||
834 | 264 | # This should never occur in the real world, but it's tested here to | ||
835 | 265 | # demonstrate this function behavior. | ||
836 | 266 | self.assertEqual(([], []), watchers.unit_machine_changes([])) | ||
837 | 0 | 267 | ||
838 | === modified file 'quickstart/utils.py' | |||
839 | --- quickstart/utils.py 2014-01-10 15:30:49 +0000 | |||
840 | +++ quickstart/utils.py 2014-01-17 18:31:35 +0000 | |||
841 | @@ -341,18 +341,6 @@ | |||
842 | 341 | return decorated | 341 | return decorated |
843 | 342 | 342 | ||
844 | 343 | 343 | ||
845 | 344 | def unit_changes(unit_name, changeset): | ||
846 | 345 | """Parse the changeset and return the change related to the given unit. | ||
847 | 346 | |||
848 | 347 | This function is intended to be used as a processor callable for the | ||
849 | 348 | watch_changes method of quickstart.juju.Environment. | ||
850 | 349 | """ | ||
851 | 350 | for change in changeset: | ||
852 | 351 | entity, _, data = change | ||
853 | 352 | if entity == 'unit' and data['Name'] == unit_name: | ||
854 | 353 | return change | ||
855 | 354 | |||
856 | 355 | |||
857 | 356 | def urlread(url): | 344 | def urlread(url): |
858 | 357 | """Open the given URL and return the page contents. | 345 | """Open the given URL and return the page contents. |
859 | 358 | 346 | ||
860 | 359 | 347 | ||
861 | === added file 'quickstart/watchers.py' | |||
862 | --- quickstart/watchers.py 1970-01-01 00:00:00 +0000 | |||
863 | +++ quickstart/watchers.py 2014-01-17 18:31:35 +0000 | |||
864 | @@ -0,0 +1,118 @@ | |||
865 | 1 | # This file is part of the Juju Quickstart Plugin, which lets users set up a | ||
866 | 2 | # Juju environment in very few steps (https://launchpad.net/juju-quickstart). | ||
867 | 3 | # Copyright (C) 2014 Canonical Ltd. | ||
868 | 4 | # | ||
869 | 5 | # This program is free software: you can redistribute it and/or modify it under | ||
870 | 6 | # the terms of the GNU Affero General Public License version 3, as published by | ||
871 | 7 | # the Free Software Foundation. | ||
872 | 8 | # | ||
873 | 9 | # This program is distributed in the hope that it will be useful, but WITHOUT | ||
874 | 10 | # ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, | ||
875 | 11 | # SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | ||
876 | 12 | # Affero General Public License for more details. | ||
877 | 13 | # | ||
878 | 14 | # You should have received a copy of the GNU Affero General Public License | ||
879 | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
880 | 16 | |||
881 | 17 | """Juju Quickstart utilities for watching Juju environments.""" | ||
882 | 18 | |||
883 | 19 | from __future__ import ( | ||
884 | 20 | print_function, | ||
885 | 21 | unicode_literals, | ||
886 | 22 | ) | ||
887 | 23 | |||
888 | 24 | |||
889 | 25 | def parse_machine_change(action, data, current_status): | ||
890 | 26 | """Parse the given machine change. | ||
891 | 27 | |||
892 | 28 | The change is represented by the given action/data pair. | ||
893 | 29 | Also receive the last known machine status, which can be an empty string | ||
894 | 30 | if the information is unknown. | ||
895 | 31 | |||
896 | 32 | Output a human readable message each time a relevant change is found. | ||
897 | 33 | |||
898 | 34 | Return the machine status. | ||
899 | 35 | Raise a ValueError if the machine is removed or in an error state. | ||
900 | 36 | """ | ||
901 | 37 | machine_id = data['Id'] | ||
902 | 38 | status = data['Status'] | ||
903 | 39 | # Exit with an error if the machine is removed. | ||
904 | 40 | if action == 'remove': | ||
905 | 41 | msg = 'machine {} unexpectedly removed'.format(machine_id) | ||
906 | 42 | raise ValueError(msg.encode('utf-8')) | ||
907 | 43 | if 'error' in status: | ||
908 | 44 | msg = 'machine {} is in an error state: {}: {}'.format( | ||
909 | 45 | machine_id, status, data['StatusInfo']) | ||
910 | 46 | raise ValueError(msg.encode('utf-8')) | ||
911 | 47 | # Notify status changes. | ||
912 | 48 | if status != current_status: | ||
913 | 49 | if status == 'pending': | ||
914 | 50 | print('machine {} provisioning is pending'.format( | ||
915 | 51 | machine_id)) | ||
916 | 52 | elif status == 'started': | ||
917 | 53 | print('machine {} is started'.format(machine_id)) | ||
918 | 54 | return status | ||
919 | 55 | |||
920 | 56 | |||
921 | 57 | def parse_unit_change(action, data, current_status, address): | ||
922 | 58 | """Parse the given unit change. | ||
923 | 59 | |||
924 | 60 | The change is represented by the given action/data pair. | ||
925 | 61 | Also receive the last known unit status and address, which can be empty | ||
926 | 62 | strings if those pieces of information are unknown. | ||
927 | 63 | |||
928 | 64 | Output a human readable message each time a relevant change is found. | ||
929 | 65 | |||
930 | 66 | Return the unit status, address and machine identifier. | ||
931 | 67 | Raise a ValueError if the service unit is removed or in an error state. | ||
932 | 68 | """ | ||
933 | 69 | unit_name = data['Name'] | ||
934 | 70 | # Exit with an error if the unit is removed. | ||
935 | 71 | if action == 'remove': | ||
936 | 72 | msg = '{} unexpectedly removed'.format(unit_name) | ||
937 | 73 | raise ValueError(msg.encode('utf-8')) | ||
938 | 74 | # Exit with an error if the unit is in an error state. | ||
939 | 75 | status = data['Status'] | ||
940 | 76 | if 'error' in status: | ||
941 | 77 | msg = '{} is in an error state: {}: {}'.format( | ||
942 | 78 | unit_name, status, data['StatusInfo']) | ||
943 | 79 | raise ValueError(msg.encode('utf-8')) | ||
944 | 80 | # Notify when the unit becomes reachable. | ||
945 | 81 | if not address: | ||
946 | 82 | address = data['PublicAddress'] | ||
947 | 83 | if address: | ||
948 | 84 | print('setting up {} on {}'.format(unit_name, address)) | ||
949 | 85 | # Notify status changes. | ||
950 | 86 | if status != current_status: | ||
951 | 87 | if status == 'pending': | ||
952 | 88 | print('{} deployment is pending'.format(unit_name)) | ||
953 | 89 | elif status == 'installed': | ||
954 | 90 | print('{} is installed'.format(unit_name)) | ||
955 | 91 | elif address and status == 'started': | ||
956 | 92 | print('{} is ready on machine {}'.format( | ||
957 | 93 | unit_name, data['MachineId'])) | ||
958 | 94 | return status, address, data.get('MachineId', '') | ||
959 | 95 | |||
960 | 96 | |||
961 | 97 | def unit_machine_changes(changeset): | ||
962 | 98 | """Parse the changeset and return the units and machines related changes. | ||
963 | 99 | |||
964 | 100 | Changes to units and machines are grouped into two lists, e.g.: | ||
965 | 101 | |||
966 | 102 | unit_changes, machine_changes = unit_machine_changes(changeset) | ||
967 | 103 | |||
968 | 104 | Each list includes (action, data) tuples, in which: | ||
969 | 105 | - action is he change type (e.g. "change", "remove"); | ||
970 | 106 | - data is the actual information about the changed entity (as a dict). | ||
971 | 107 | |||
972 | 108 | This function is intended to be used as a processor callable for the | ||
973 | 109 | watch_changes method of quickstart.juju.Environment. | ||
974 | 110 | """ | ||
975 | 111 | unit_changes = [] | ||
976 | 112 | machine_changes = [] | ||
977 | 113 | for entity, action, data in changeset: | ||
978 | 114 | if entity == 'unit': | ||
979 | 115 | unit_changes.append((action, data)) | ||
980 | 116 | elif entity == 'machine': | ||
981 | 117 | machine_changes.append((action, data)) | ||
982 | 118 | return unit_changes, machine_changes |
Reviewers: mp+202105_ code.launchpad. net,
Message:
Please take a look.
Description:
Improve machine errors handling.
Quickstart no longer hangs when the GUI
machine goes in an "error" state and, as
a consequence, the unit is forever in
a "pending" state.
Also fixed the message printed at the
end of the process suggesting how to
destroy the environment
(sudo is used where required).
Tests: `make check`.
QA: I already asked Rick to reproduce the trusty/LXC
errors he encountered QAing my previous branch.
The GUI no longer hangs.
I'd also appreciate any other suggestion and additional
QA, given that this branch is a good candidate for 1.0.
https:/ /code.launchpad .net/~frankban/ juju-quickstart /handle- machine- errors/ +merge/ 202105
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/51350044/
Affected files (+616, -180 lines): manage. py tests/test_ app.py tests/test_ utils.py tests/test_ watchers. py watchers. py
A [revision details]
M quickstart/app.py
M quickstart/
M quickstart/
M quickstart/
A quickstart/
M quickstart/utils.py
A quickstart/