Merge lp:~frankban/charms/precise/juju-gui/cancel-deployment into lp:~juju-gui/charms/precise/juju-gui/trunk
- Precise Pangolin (12.04)
- cancel-deployment
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 108 |
Proposed branch: | lp:~frankban/charms/precise/juju-gui/cancel-deployment |
Merge into: | lp:~juju-gui/charms/precise/juju-gui/trunk |
Diff against target: |
542 lines (+245/-33) 9 files modified
revision (+1/-1) server/guiserver/__init__.py (+1/-1) server/guiserver/bundles/__init__.py (+46/-13) server/guiserver/bundles/base.py (+39/-5) server/guiserver/bundles/utils.py (+7/-0) server/guiserver/bundles/views.py (+22/-0) server/guiserver/tests/bundles/test_base.py (+63/-1) server/guiserver/tests/bundles/test_utils.py (+29/-12) server/guiserver/tests/bundles/test_views.py (+37/-0) |
To merge this branch: | bzr merge lp:~frankban/charms/precise/juju-gui/cancel-deployment |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
charmers | Pending | ||
Review via email:
|
Commit message
Description of the change
GUI server: cancel deployment feature.
Added an API call for cancelling a pending
deployment.
Also updated the bundles module documentation.
For this first implementation, I discussed
with Gary a workaround to avoid scheduled
deployments to be included in the ProcessPool
executor's call queue: a time.sleep call is
added to the queue right after a new deployment.
This way, as described in the code, it is still
possible to cancel a scheduled deployment job
even if it is the first in the queue.
Tests:
run `make unittest` from the root of this branch.
QA:
- bootstrap a juju-core environment;
- deploy the GUI from this branch (`make deploy`);
- switch to the builtin server:
`juju set juju-gui builtin-
- ensure the GUI is working well by visiting
https:/
To test the deployer support and the
"cancel deployment" feature, use the script in
http://
- download and save the Python script;
- run it passing the GUI node address as first argument:
`python start-deployer-
The script does the following:
1) it logs in to the juju-core API server;
2) it starts/schedules three deployments;
3) it send two invalid Cancel requests;
4) it cancels the second deployment;
5) it shows the deployments status before and
after the deployment deletion.
- if everything is ok, you should see an output like
this: http://
- you should also be able to check the deployment
progress from the GUI;
- when the two deployments are completed (it may take
some minutes) you should see wordpress, mysql and
mediawiki correctly deployed and displayed in the
topology view;
- visiting https:/
should see something like this:
{
"uptime": 970,
"deployer": [
{"Status": "completed", "DeploymentId": 0, "Time": 1379062144},
{"Status": "cancelled", "DeploymentId": 1, "Time": 1379061766},
{"Status": "completed", "DeploymentId": 2, "Time": 1379062156}
],
"apiversion": "go",
"sandbox": false,
"version": "0.2.0",
"debug": false,
"apiurl": "wss://
}
That's all, thanks a lot for QAing this branch!
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Francesco Banconi (frankban) wrote : | # |
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Francesco Banconi (frankban) wrote : | # |
I forgot to mention that you need to change the PASSWORD
at line #17 of the script, setting your own admin secret,
before running the script itself.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Richard Harding (rharding) wrote : | # |
code ok, will qa next. Just have the one objection to camel cased http
params.
https:/
File server/
https:/
server/
request.
caps in the query string? why not match the var name used deployment_id?
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Francesco Banconi (frankban) wrote : | # |
On 2013/09/13 13:16:21, rharding wrote:
> server/
> request.
> caps in the query string? why not match the var name used
deployment_id?
params is not really a querystring, it's the content of the Params field
included in the WebSocket message. Caps are used for consistency with
all
the other juju-core API calls.
- 116. By Francesco Banconi
-
Debugging a unit test.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Richard Harding (rharding) wrote : | # |
On 2013/09/13 13:24:32, frankban wrote:
> On 2013/09/13 13:16:21, rharding wrote:
> > server/
> > request.
> > caps in the query string? why not match the var name used
deployment_id?
> params is not really a querystring, it's the content of the Params
field
> included in the WebSocket message. Caps are used for consistency with
all
> the other juju-core API calls.
Ah, ok. In pyramid/charmworld request.params is a map into
request.GET/POST.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Richard Harding (rharding) wrote : | # |
LGTM
Results of the script, first run:
http://
I did run it a second time and got an error:
http://
I'm not sure if this is a bug here or as a follow up to not crash when
the service is already there.
- 117. By Francesco Banconi
-
Debugging again.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Francesco Banconi (frankban) wrote : | # |
On 2013/09/13 13:45:00, rharding wrote:
> LGTM
> Results of the script, first run:
> http://
Great, thank you for QAing this branch!
> I did run it a second time and got an error:
> http://
> I'm not sure if this is a bug here or as a follow up to not crash when
the
> service is already there.
Running the script a second time you also tested the validation
step failing when there is a service name clash in the
environment, and that's the correct behavior.
The subsequent KeyError is ok since the script expects
the request to be successful.
Thanks!
- 118. By Francesco Banconi
-
Final debugging.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Brad Crittenden (bac) wrote : | # |
LGTM
https:/
File server/
https:/
server/
if the job is the next to be started.
It's not clear to my why you want to be able to cancel this first job.
Only giving a window of 1 second seems arbitrary. Perhaps the use case
will become clearer as I read more code.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Francesco Banconi (frankban) wrote : | # |
*** Submitted:
GUI server: cancel deployment feature.
Added an API call for cancelling a pending
deployment.
Also updated the bundles module documentation.
For this first implementation, I discussed
with Gary a workaround to avoid scheduled
deployments to be included in the ProcessPool
executor's call queue: a time.sleep call is
added to the queue right after a new deployment.
This way, as described in the code, it is still
possible to cancel a scheduled deployment job
even if it is the first in the queue.
Tests:
run `make unittest` from the root of this branch.
QA:
- bootstrap a juju-core environment;
- deploy the GUI from this branch (`make deploy`);
- switch to the builtin server:
`juju set juju-gui builtin-
- ensure the GUI is working well by visiting
https:/
To test the deployer support and the
"cancel deployment" feature, use the script in
http://
- download and save the Python script;
- run it passing the GUI node address as first argument:
`python start-deployer-
The script does the following:
1) it logs in to the juju-core API server;
2) it starts/schedules three deployments;
3) it send two invalid Cancel requests;
4) it cancels the second deployment;
5) it shows the deployments status before and
after the deployment deletion.
- if everything is ok, you should see an output like
this: http://
- you should also be able to check the deployment
progress from the GUI;
- when the two deployments are completed (it may take
some minutes) you should see wordpress, mysql and
mediawiki correctly deployed and displayed in the
topology view;
- visiting https:/
should see something like this:
{
"uptime": 970,
"deployer": [
{"Status": "completed", "DeploymentId": 0, "Time": 1379062144},
{"Status": "cancelled", "DeploymentId": 1, "Time": 1379061766},
{"Status": "completed", "DeploymentId": 2, "Time": 1379062156}
],
"apiversion": "go",
"sandbox": false,
"version": "0.2.0",
"debug": false,
"apiurl": "wss://
}
That's all, thanks a lot for QAing this branch!
R=rharding, bac
CC=
https:/
https:/
File server/
https:/
server/
if the job is the next to be started.
On 2013/09/13 14:33:19, bac wrote:
> It's not clear to my why you want to be able to cancel this first job.
Only
> giving a window of 1 second seems arbitrary. Perhaps the use case
will become
> clearer as I read more code.
It is arbitrary indeed. This is just a way to fill the call queue so
that, when a job starts (and it is removed from the queue) he sleep is
put in the queue and the next deployment can still be cancelled. This is
just a workaround, and will be changed in the future, likely
implementing our own as...
Preview Diff
1 | === modified file 'revision' |
2 | --- revision 2013-09-12 19:58:07 +0000 |
3 | +++ revision 2013-09-13 14:09:53 +0000 |
4 | @@ -1,1 +1,1 @@ |
5 | -84 |
6 | +85 |
7 | |
8 | === modified file 'server/guiserver/__init__.py' |
9 | --- server/guiserver/__init__.py 2013-08-23 14:44:00 +0000 |
10 | +++ server/guiserver/__init__.py 2013-09-13 14:09:53 +0000 |
11 | @@ -30,7 +30,7 @@ |
12 | the HTTPS connection, allowing changes in the Juju environment to be propagated |
13 | and shown immediately by the browser. """ |
14 | |
15 | -VERSION = (0, 1, 0) |
16 | +VERSION = (0, 2, 0) |
17 | |
18 | |
19 | def get_version(): |
20 | |
21 | === modified file 'server/guiserver/bundles/__init__.py' |
22 | --- server/guiserver/bundles/__init__.py 2013-09-04 15:21:54 +0000 |
23 | +++ server/guiserver/bundles/__init__.py 2013-09-13 14:09:53 +0000 |
24 | @@ -16,9 +16,6 @@ |
25 | |
26 | """Juju GUI server bundles support. |
27 | |
28 | -XXX frankban: note that the following is a work in progress. Some of the |
29 | -objects described below are not yet implemented. |
30 | - |
31 | This package includes the objects and functions required to support deploying |
32 | bundles in juju-core. The base pieces of the infrastructure are placed in the |
33 | base module: |
34 | @@ -195,7 +192,9 @@ |
35 | bundle at the time. A Queue value of zero means the deployment will be started |
36 | as soon as possible. |
37 | |
38 | -The Status can be one of the following: 'scheduled', 'started' and 'completed'. |
39 | +The Status can be one of the following: 'scheduled', 'started', 'completed' and |
40 | +'cancelled. See the next section for an explanation of how to cancel a pending |
41 | +(scheduled) deployment. |
42 | |
43 | The Time field indicates the number of seconds since the epoch at the time of |
44 | the change. |
45 | @@ -222,6 +221,40 @@ |
46 | XXX frankban: a timeout to delete completed deployments history will be |
47 | eventually implemented. |
48 | |
49 | +Cancelling a deployment. |
50 | +------------------------ |
51 | + |
52 | +It is possible to cancel the execution of scheduled deployments by sending a |
53 | +Cancel request, e.g.: |
54 | + |
55 | + { |
56 | + 'RequestId': 5, |
57 | + 'Type': 'Deployer', |
58 | + 'Request': 'Cancel', |
59 | + 'Params': {'DeploymentId': 42}, |
60 | + } |
61 | + |
62 | +Note that it is allowed to cancel a deployment only if it is not yet started, |
63 | +i.e. if it is in a 'scheduled' state. |
64 | + |
65 | +If any error occurs, the response is like this: |
66 | + |
67 | + { |
68 | + 'RequestId': 5, |
69 | + 'Response': {}, |
70 | + 'Error': 'some error: error details', |
71 | + } |
72 | + |
73 | +Usually an error response is returned when either an invalid deployment id was |
74 | +provided or the request attempted to cancel an already started deployment. |
75 | + |
76 | +If the deployment is successfully cancelled, the response is the following: |
77 | + |
78 | + { |
79 | + 'RequestId': 5, |
80 | + 'Response': {}, |
81 | + } |
82 | + |
83 | Deployments status. |
84 | ------------------- |
85 | |
86 | @@ -229,7 +262,7 @@ |
87 | the client can send the following request: |
88 | |
89 | { |
90 | - 'RequestId': 5, |
91 | + 'RequestId': 6, |
92 | 'Type': 'Deployer', |
93 | 'Request': 'Status', |
94 | } |
95 | @@ -238,29 +271,29 @@ |
96 | the second one is a successful response: |
97 | |
98 | { |
99 | - 'RequestId': 5, |
100 | + 'RequestId': 6, |
101 | 'Response': {}, |
102 | 'Error': 'some error: error details', |
103 | } |
104 | |
105 | { |
106 | - 'RequestId': 5, |
107 | + 'RequestId': 6, |
108 | 'Response': { |
109 | 'LastChanges': [ |
110 | - {'DeploymentId': 42, 'Status': 'completed', 'Time': 1377080001, |
111 | + {'DeploymentId': 1, 'Status': 'completed', 'Time': 1377080001, |
112 | 'Error': 'error'}, |
113 | - {'DeploymentId': 43, 'Status': 'completed', |
114 | - 'Time': 1377080002}, |
115 | - {'DeploymentId': 44, 'Status': 'started', 'Time': 1377080003, |
116 | + {'DeploymentId': 2, 'Status': 'completed', 'Time': 1377080002}, |
117 | + {'DeploymentId': 3, 'Status': 'started', 'Time': 1377080003, |
118 | 'Queue': 0}, |
119 | - {'DeploymentId': 45, 'Status': 'scheduled', 'Time': 1377080004, |
120 | + {'DeploymentId': 4, 'Status': 'cancelled', 'Time': 1377080004}, |
121 | + {'DeploymentId': 5, 'Status': 'scheduled', 'Time': 1377080005, |
122 | 'Queue': 1}, |
123 | ], |
124 | }, |
125 | } |
126 | |
127 | In the second response above, the Error field in the first attempted deployment |
128 | -(42) contains details about an error that occurred while deploying a bundle. |
129 | +(1) contains details about an error that occurred while deploying a bundle. |
130 | This means that bundle deployment has been completed but an error occurred |
131 | during the process. |
132 | """ |
133 | |
134 | === modified file 'server/guiserver/bundles/base.py' |
135 | --- server/guiserver/bundles/base.py 2013-08-26 07:56:52 +0000 |
136 | +++ server/guiserver/bundles/base.py 2013-09-13 14:09:53 +0000 |
137 | @@ -23,7 +23,12 @@ |
138 | a detailed explanation of how these objects are used. |
139 | """ |
140 | |
141 | -from concurrent.futures import ProcessPoolExecutor |
142 | +import time |
143 | + |
144 | +from concurrent.futures import ( |
145 | + process, |
146 | + ProcessPoolExecutor, |
147 | +) |
148 | from tornado import gen |
149 | from tornado.ioloop import IOLoop |
150 | from tornado.util import ObjectDict |
151 | @@ -37,6 +42,10 @@ |
152 | from guiserver.watchers import WatcherError |
153 | |
154 | |
155 | +# Controls how many more calls than processes will be queued in the call queue. |
156 | +# Set to zero to make Future.cancel() succeed more frequently (Futures in the |
157 | +# call queue cannot be cancelled). |
158 | +process.EXTRA_QUEUED_CALLS = 0 |
159 | # Juju API versions supported by the GUI server Deployer. |
160 | # Tests use the first API version in this list. |
161 | SUPPORTED_API_VERSIONS = ['go'] |
162 | @@ -78,6 +87,8 @@ |
163 | # Queue stores the deployment identifiers corresponding to the |
164 | # currently started/queued jobs. |
165 | self._queue = [] |
166 | + # The futures attribute maps deployment identifiers to Futures. |
167 | + self._futures = {} |
168 | |
169 | @gen.coroutine |
170 | def validate(self, user, name, bundle): |
171 | @@ -132,9 +143,14 @@ |
172 | future = self._run_executor.submit( |
173 | blocking.import_bundle, self._apiurl, user.password, name, bundle) |
174 | add_future(self._io_loop, future, self._import_callback, deployment_id) |
175 | + self._futures[deployment_id] = future |
176 | # If a customized callback is provided, schedule it as well. |
177 | if test_callback is not None: |
178 | add_future(self._io_loop, future, test_callback) |
179 | + # Submit a sleeping job in order to avoid the next deployment job to be |
180 | + # immediately put in the executor's call queue. This allows for |
181 | + # cancelling scheduled jobs, even if the job is the next to be started. |
182 | + self._run_executor.submit(time.sleep, 1) |
183 | return deployment_id |
184 | |
185 | def _import_callback(self, deployment_id, future): |
186 | @@ -144,12 +160,17 @@ |
187 | deployment_id identifying one specific deployment job, and the fired |
188 | future returned by the executor. |
189 | """ |
190 | - exception = future.exception() |
191 | - error = None if exception is None else str(exception) |
192 | - # Notify a deployment completed. |
193 | - self._observer.notify_completed(deployment_id, error=error) |
194 | + if future.cancelled(): |
195 | + # Notify a deployment has been cancelled. |
196 | + self._observer.notify_cancelled(deployment_id) |
197 | + else: |
198 | + exception = future.exception() |
199 | + error = None if exception is None else str(exception) |
200 | + # Notify a deployment completed. |
201 | + self._observer.notify_completed(deployment_id, error=error) |
202 | # Remove the completed deployment job from the queue. |
203 | self._queue.remove(deployment_id) |
204 | + del self._futures[deployment_id] |
205 | # Notify the new position of all remaining deployments in the queue. |
206 | for position, deploy_id in enumerate(self._queue): |
207 | self._observer.notify_position(deploy_id, position) |
208 | @@ -183,6 +204,18 @@ |
209 | except WatcherError: |
210 | return |
211 | |
212 | + def cancel(self, deployment_id): |
213 | + """Attempt to cancel the deployment identified by deployment_id. |
214 | + |
215 | + Return None if the deployment has been correctly cancelled. |
216 | + Return an error string otherwise. |
217 | + """ |
218 | + future = self._futures.get(deployment_id) |
219 | + if future is None: |
220 | + return 'deployment not found or already completed' |
221 | + if not future.cancel(): |
222 | + return 'unable to cancel the deployment' |
223 | + |
224 | def status(self): |
225 | """Return a list containing the last known change for each deployment. |
226 | """ |
227 | @@ -221,6 +254,7 @@ |
228 | 'Import': views.import_bundle, |
229 | 'Watch': views.watch, |
230 | 'Next': views.next, |
231 | + 'Cancel': views.cancel, |
232 | 'Status': views.status, |
233 | } |
234 | |
235 | |
236 | === modified file 'server/guiserver/bundles/utils.py' |
237 | --- server/guiserver/bundles/utils.py 2013-08-21 16:12:45 +0000 |
238 | +++ server/guiserver/bundles/utils.py 2013-09-13 14:09:53 +0000 |
239 | @@ -29,6 +29,7 @@ |
240 | # Change statuses. |
241 | SCHEDULED = 'scheduled' |
242 | STARTED = 'started' |
243 | +CANCELLED = 'cancelled' |
244 | COMPLETED = 'completed' |
245 | |
246 | |
247 | @@ -99,6 +100,12 @@ |
248 | change = create_change(deployment_id, status, queue=position) |
249 | watcher.put(change) |
250 | |
251 | + def notify_cancelled(self, deployment_id): |
252 | + """Add a change to the deployment watcher notifying it is cancelled.""" |
253 | + watcher = self.deployments[deployment_id] |
254 | + change = create_change(deployment_id, CANCELLED) |
255 | + watcher.close(change) |
256 | + |
257 | def notify_completed(self, deployment_id, error=None): |
258 | """Add a change to the deployment watcher notifying it is completed.""" |
259 | watcher = self.deployments[deployment_id] |
260 | |
261 | === modified file 'server/guiserver/bundles/views.py' |
262 | --- server/guiserver/bundles/views.py 2013-09-04 15:22:42 +0000 |
263 | +++ server/guiserver/bundles/views.py 2013-09-13 14:09:53 +0000 |
264 | @@ -169,6 +169,28 @@ |
265 | |
266 | @gen.coroutine |
267 | @require_authenticated_user |
268 | +def cancel(request, deployer): |
269 | + """Cancel the given pending deployment. |
270 | + |
271 | + The deployment is identified in the request by the DeploymentId parameter. |
272 | + If the request is not valid or the deployment cannot be cancelled (e.g. |
273 | + because it is already started) an error response is returned. |
274 | + |
275 | + Request: 'Cancel'. |
276 | + Parameters example: {'DeploymentId': 42}. |
277 | + """ |
278 | + deployment_id = request.params.get('DeploymentId') |
279 | + if deployment_id is None: |
280 | + raise response(error='invalid request: invalid data parameters') |
281 | + # Use the Deployer instance to cancel the deployment. |
282 | + err = deployer.cancel(deployment_id) |
283 | + if err is not None: |
284 | + raise response(error='invalid request: {}'.format(err)) |
285 | + raise response() |
286 | + |
287 | + |
288 | +@gen.coroutine |
289 | +@require_authenticated_user |
290 | def status(request, deployer): |
291 | """Return the current status of all the bundle deployments. |
292 | |
293 | |
294 | === modified file 'server/guiserver/tests/bundles/test_base.py' |
295 | --- server/guiserver/tests/bundles/test_base.py 2013-08-26 07:56:52 +0000 |
296 | +++ server/guiserver/tests/bundles/test_base.py 2013-09-13 14:09:53 +0000 |
297 | @@ -195,13 +195,75 @@ |
298 | # Wait for the deployment to be completed. |
299 | self.wait() |
300 | |
301 | - @gen_test |
302 | def test_invalid_watcher(self): |
303 | # None is returned if the watcher id is not valid. |
304 | deployer = self.make_deployer() |
305 | changes = deployer.next(42) |
306 | self.assertIsNone(changes) |
307 | |
308 | + @gen_test |
309 | + def test_cancel(self): |
310 | + # It is possible to cancel the execution of a pending deployment. |
311 | + deployer = self.make_deployer() |
312 | + with self.patch_import_bundle(): |
313 | + # The test callback is passed to the first deployment because we |
314 | + # expect the second one to be immediately cancelled. |
315 | + deployer.import_bundle( |
316 | + self.user, 'bundle', self.bundle, test_callback=self.stop) |
317 | + deployment_id = deployer.import_bundle( |
318 | + self.user, 'bundle', self.bundle) |
319 | + watcher_id = deployer.watch(deployment_id) |
320 | + self.assertIsNone(deployer.cancel(deployment_id)) |
321 | + # We expect two changes: the second one should notify the deployment |
322 | + # has been cancelled. |
323 | + yield deployer.next(watcher_id) |
324 | + changes = yield deployer.next(watcher_id) |
325 | + self.assert_change(changes, deployment_id, utils.CANCELLED) |
326 | + # Wait for the deployment to be completed. |
327 | + self.wait() |
328 | + |
329 | + def test_cancel_unknown_deployment(self): |
330 | + # An error is returned when trying to cancel an invalid deployment. |
331 | + deployer = self.make_deployer() |
332 | + error = deployer.cancel(42) |
333 | + self.assertEqual('deployment not found or already completed', error) |
334 | + |
335 | + @gen_test |
336 | + def test_cancel_completed_deployment(self): |
337 | + # An error is returned when trying to cancel a completed deployment. |
338 | + deployer = self.make_deployer() |
339 | + with self.patch_import_bundle(): |
340 | + deployment_id = deployer.import_bundle( |
341 | + self.user, 'bundle', self.bundle, test_callback=self.stop) |
342 | + watcher_id = deployer.watch(deployment_id) |
343 | + # Assume the deployment is completed after two changes. |
344 | + yield deployer.next(watcher_id) |
345 | + yield deployer.next(watcher_id) |
346 | + error = deployer.cancel(deployment_id) |
347 | + self.assertEqual('deployment not found or already completed', error) |
348 | + # Wait for the deployment to be completed. |
349 | + self.wait() |
350 | + |
351 | + @gen_test |
352 | + def test_cancel_started_deployment(self): |
353 | + # An error is returned when trying to cancel a deployment already |
354 | + # started. |
355 | + deployer = self.make_deployer() |
356 | + with self.patch_import_bundle() as mock_import_bundle: |
357 | + deployment_id = deployer.import_bundle( |
358 | + self.user, 'bundle', self.bundle, test_callback=self.stop) |
359 | + watcher_id = deployer.watch(deployment_id) |
360 | + # Wait until the deployment is started. |
361 | + yield deployer.next(watcher_id) |
362 | + while True: |
363 | + if mock_import_bundle.call_count: |
364 | + break |
365 | + error = deployer.cancel(deployment_id) |
366 | + self.assertEqual('unable to cancel the deployment', error) |
367 | + # Wait for the deployment to be completed. |
368 | + yield deployer.next(watcher_id) |
369 | + self.wait() |
370 | + |
371 | def test_initial_status(self): |
372 | # The initial deployer status is an empty list. |
373 | deployer = self.make_deployer() |
374 | |
375 | === modified file 'server/guiserver/tests/bundles/test_utils.py' |
376 | --- server/guiserver/tests/bundles/test_utils.py 2013-08-21 16:12:45 +0000 |
377 | +++ server/guiserver/tests/bundles/test_utils.py 2013-09-13 14:09:53 +0000 |
378 | @@ -32,12 +32,15 @@ |
379 | from guiserver.tests import helpers |
380 | |
381 | |
382 | -@mock.patch('time.time', mock.Mock(return_value=1234)) |
383 | +mock_time = mock.patch('time.time', mock.Mock(return_value=12345)) |
384 | + |
385 | + |
386 | +@mock_time |
387 | class TestCreateChange(unittest.TestCase): |
388 | |
389 | def test_status(self): |
390 | # The change includes the deployment status. |
391 | - expected = {'DeploymentId': 0, 'Status': utils.STARTED, 'Time': 1234} |
392 | + expected = {'DeploymentId': 0, 'Status': utils.STARTED, 'Time': 12345} |
393 | obtained = utils.create_change(0, utils.STARTED) |
394 | self.assertEqual(expected, obtained) |
395 | |
396 | @@ -46,7 +49,7 @@ |
397 | expected = { |
398 | 'DeploymentId': 1, |
399 | 'Status': utils.SCHEDULED, |
400 | - 'Time': 1234, |
401 | + 'Time': 12345, |
402 | 'Queue': 42, |
403 | } |
404 | obtained = utils.create_change(1, utils.SCHEDULED, queue=42) |
405 | @@ -57,7 +60,7 @@ |
406 | expected = { |
407 | 'DeploymentId': 2, |
408 | 'Status': utils.COMPLETED, |
409 | - 'Time': 1234, |
410 | + 'Time': 12345, |
411 | 'Error': 'an error', |
412 | } |
413 | obtained = utils.create_change(2, utils.COMPLETED, error='an error') |
414 | @@ -68,7 +71,7 @@ |
415 | expected = { |
416 | 'DeploymentId': 3, |
417 | 'Status': utils.COMPLETED, |
418 | - 'Time': 1234, |
419 | + 'Time': 12345, |
420 | 'Queue': 47, |
421 | 'Error': 'an error', |
422 | } |
423 | @@ -139,7 +142,7 @@ |
424 | self.assert_watcher(watcher1, deployment1) |
425 | self.assert_watcher(watcher2, deployment2) |
426 | |
427 | - @mock.patch('time.time', mock.Mock(return_value=1234)) |
428 | + @mock_time |
429 | def test_notify_scheduled(self): |
430 | # It is possible to notify a new queue position for a deployment. |
431 | deployment_id = self.observer.add_deployment() |
432 | @@ -148,13 +151,13 @@ |
433 | expected = { |
434 | 'DeploymentId': deployment_id, |
435 | 'Status': utils.SCHEDULED, |
436 | - 'Time': 1234, |
437 | + 'Time': 12345, |
438 | 'Queue': 3, |
439 | } |
440 | self.assertEqual(expected, watcher.getlast()) |
441 | self.assertFalse(watcher.closed) |
442 | |
443 | - @mock.patch('time.time', mock.Mock(return_value=12345)) |
444 | + @mock_time |
445 | def test_notify_started(self): |
446 | # It is possible to notify that a deployment is (about to be) started. |
447 | deployment_id = self.observer.add_deployment() |
448 | @@ -169,7 +172,21 @@ |
449 | self.assertEqual(expected, watcher.getlast()) |
450 | self.assertFalse(watcher.closed) |
451 | |
452 | - @mock.patch('time.time', mock.Mock(return_value=123456)) |
453 | + @mock_time |
454 | + def test_notify_cancelled(self): |
455 | + # It is possible to notify that a deployment has been cancelled. |
456 | + deployment_id = self.observer.add_deployment() |
457 | + watcher = self.observer.deployments[deployment_id] |
458 | + self.observer.notify_cancelled(deployment_id) |
459 | + expected = { |
460 | + 'DeploymentId': deployment_id, |
461 | + 'Status': utils.CANCELLED, |
462 | + 'Time': 12345, |
463 | + } |
464 | + self.assertEqual(expected, watcher.getlast()) |
465 | + self.assertTrue(watcher.closed) |
466 | + |
467 | + @mock_time |
468 | def test_notify_completed(self): |
469 | # It is possible to notify that a deployment is completed. |
470 | deployment_id = self.observer.add_deployment() |
471 | @@ -178,12 +195,12 @@ |
472 | expected = { |
473 | 'DeploymentId': deployment_id, |
474 | 'Status': utils.COMPLETED, |
475 | - 'Time': 123456, |
476 | + 'Time': 12345, |
477 | } |
478 | self.assertEqual(expected, watcher.getlast()) |
479 | self.assertTrue(watcher.closed) |
480 | |
481 | - @mock.patch('time.time', mock.Mock(return_value=1234567)) |
482 | + @mock_time |
483 | def test_notify_error(self): |
484 | # It is possible to notify that an error occurred during a deployment. |
485 | deployment_id = self.observer.add_deployment() |
486 | @@ -192,7 +209,7 @@ |
487 | expected = { |
488 | 'DeploymentId': deployment_id, |
489 | 'Status': utils.COMPLETED, |
490 | - 'Time': 1234567, |
491 | + 'Time': 12345, |
492 | 'Error': 'bad wolf', |
493 | } |
494 | self.assertEqual(expected, watcher.getlast()) |
495 | |
496 | === modified file 'server/guiserver/tests/bundles/test_views.py' |
497 | --- server/guiserver/tests/bundles/test_views.py 2013-09-04 10:59:44 +0000 |
498 | +++ server/guiserver/tests/bundles/test_views.py 2013-09-13 14:09:53 +0000 |
499 | @@ -267,6 +267,43 @@ |
500 | self.deployer.next.assert_called_once_with(42) |
501 | |
502 | |
503 | +class TestCancel( |
504 | + ViewsTestMixin, helpers.BundlesTestMixin, LogTrapTestCase, |
505 | + AsyncTestCase): |
506 | + |
507 | + def get_view(self): |
508 | + return views.cancel |
509 | + |
510 | + @gen_test |
511 | + def test_invalid_deployment(self): |
512 | + # An error response is returned if the deployment identifier is not |
513 | + # valid. |
514 | + request = self.make_view_request(params={'DeploymentId': 42}) |
515 | + # Set up the Deployer mock. |
516 | + self.deployer.cancel.return_value = 'bad wolf' |
517 | + # Execute the view. |
518 | + response = yield self.view(request, self.deployer) |
519 | + expected_response = { |
520 | + 'Response': {}, |
521 | + 'Error': 'invalid request: bad wolf', |
522 | + } |
523 | + self.assertEqual(expected_response, response) |
524 | + # Ensure the Deployer methods have been correctly called. |
525 | + self.deployer.cancel.assert_called_once_with(42) |
526 | + |
527 | + @gen_test |
528 | + def test_success(self): |
529 | + # An empty response is returned if everything is ok. |
530 | + request = self.make_view_request(params={'DeploymentId': 42}) |
531 | + # Set up the Deployer mock. |
532 | + self.deployer.cancel.return_value = None |
533 | + # Execute the view. |
534 | + response = yield self.view(request, self.deployer) |
535 | + self.assertEqual({'Response': {}}, response) |
536 | + # Ensure the Deployer methods have been correctly called. |
537 | + self.deployer.cancel.assert_called_once_with(42) |
538 | + |
539 | + |
540 | class TestStatus( |
541 | ViewsTestMixin, helpers.BundlesTestMixin, LogTrapTestCase, |
542 | AsyncTestCase): |
Reviewers: mp+185448_ code.launchpad. net,
Message:
Please take a look.
Description:
GUI server: cancel deployment feature.
Added an API call for cancelling a pending
deployment.
Also updated the bundles module documentation.
For this first implementation, I discussed
with Gary a workaround to avoid scheduled
deployments to be included in the ProcessPool
executor's call queue: a time.sleep call is
added to the queue right after a new deployment.
This way, as described in the code, it is still
possible to cancel a scheduled deployment job
even if it is the first in the queue.
Tests:
run `make unittest` from the root of this branch.
QA: server= true`; /GUI-ADDRESS .
- bootstrap a juju-core environment;
- deploy the GUI from this branch (`make deploy`);
- switch to the builtin server:
`juju set juju-gui builtin-
- ensure the GUI is working well by visiting
https:/
To test the deployer support and the pastebin. ubuntu. com/6100815/ e.g.:
"cancel deployment" feature, use the script in
http://
- download and save the Python script; cancel. py GUI-ADDRESS`.
- run it passing the GUI node address as first argument:
`python start-deployer-
The script does the following:
1) it logs in to the juju-core API server;
2) it starts/schedules three deployments;
3) it send two invalid Cancel requests;
4) it cancels the second deployment;
5) it shows the deployments status before and
after the deployment deletion.
- if everything is ok, you should see an output like pastebin. ubuntu. com/6100861/ ; /GUI-ADDRESS/ gui-server- info you ec2-50- 17-116- 51.compute- 1.amazonaws. com:17070"
this: http://
- you should also be able to check the deployment
progress from the GUI;
- when the two deployments are completed (it may take
some minutes) you should see wordpress, mysql and
mediawiki correctly deployed and displayed in the
topology view;
- visiting https:/
should see something like this:
{
"uptime": 970,
"deployer": [
{"Status": "completed", "DeploymentId": 0, "Time": 1379062144},
{"Status": "cancelled", "DeploymentId": 1, "Time": 1379061766},
{"Status": "completed", "DeploymentId": 2, "Time": 1379062156}
],
"apiversion": "go",
"sandbox": false,
"version": "0.2.0",
"debug": false,
"apiurl": "wss://
}
That's all, thanks a lot for QAing this branch!
https:/ /code.launchpad .net/~frankban/ charms/ precise/ juju-gui/ cancel- deployment/ +merge/ 185448
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/13549046/
Affected files (+243, -32 lines): guiserver/ __init_ _.py guiserver/ bundles/ __init_ _.py guiserver/ bundles/ base.py guiserver/ bundles/ utils.py guiserver/ bundles/ views.py guiserver/ tests/bundles/ test_base. py guiserver/ tests/bundles/ test_utils. py guiserver/ tests/bundles/ test_views. py
A [revision details]
M revision
M server/
M server/
M server/
M server/
M server/
M server/
M server/
M server/