Merge lp:~frankban/charms/precise/juju-gui/cancel-deployment into lp:~juju-gui/charms/precise/juju-gui/trunk

Proposed by Francesco Banconi
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
Reviewer Review Type Date Requested Status
charmers Pending
Review via email: mp+185448@code.launchpad.net

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-server=true`;
- ensure the GUI is working well by visiting
  https://GUI-ADDRESS .

To test the deployer support and the
"cancel deployment" feature, use the script in
http://pastebin.ubuntu.com/6100815/ e.g.:

- download and save the Python script;
- run it passing the GUI node address as first argument:
  `python start-deployer-cancel.py GUI-ADDRESS`.

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://pastebin.ubuntu.com/6100861/ ;
- 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://GUI-ADDRESS/gui-server-info you
  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://ec2-50-17-116-51.compute-1.amazonaws.com:17070"
  }

That's all, thanks a lot for QAing this branch!

https://codereview.appspot.com/13549046/

To post a comment you must log in.
Revision history for this message
Francesco Banconi (frankban) wrote :

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:
- bootstrap a juju-core environment;
- deploy the GUI from this branch (`make deploy`);
- switch to the builtin server:
   `juju set juju-gui builtin-server=true`;
- ensure the GUI is working well by visiting
   https://GUI-ADDRESS .

To test the deployer support and the
"cancel deployment" feature, use the script in
http://pastebin.ubuntu.com/6100815/ e.g.:

- download and save the Python script;
- run it passing the GUI node address as first argument:
   `python start-deployer-cancel.py GUI-ADDRESS`.

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://pastebin.ubuntu.com/6100861/ ;
- 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://GUI-ADDRESS/gui-server-info you
   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://ec2-50-17-116-51.compute-1.amazonaws.com:17070"
   }

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):
   A [revision details]
   M revision
   M server/guiserver/__init__.py
   M server/guiserver/bundles/__init__.py
   M server/guiserver/bundles/base.py
   M server/guiserver/bundles/utils.py
   M server/guiserver/bundles/views.py
   M server/guiserver/tests/bundles/test_base.py
   M server/guiserver/tests/bundles/test_utils.py
   M server/guiserver/tests/bundles/test_views.py

Revision history for this message
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.

https://codereview.appspot.com/13549046/

Revision history for this message
Richard Harding (rharding) wrote :

code ok, will qa next. Just have the one objection to camel cased http
params.

https://codereview.appspot.com/13549046/diff/1/server/guiserver/bundles/views.py
File server/guiserver/bundles/views.py (right):

https://codereview.appspot.com/13549046/diff/1/server/guiserver/bundles/views.py#newcode182
server/guiserver/bundles/views.py:182: deployment_id =
request.params.get('DeploymentId')
caps in the query string? why not match the var name used deployment_id?

https://codereview.appspot.com/13549046/

Revision history for this message
Francesco Banconi (frankban) wrote :

On 2013/09/13 13:16:21, rharding wrote:

> server/guiserver/bundles/views.py:182: deployment_id =
> request.params.get('DeploymentId')
> 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.

https://codereview.appspot.com/13549046/

116. By Francesco Banconi

Debugging a unit test.

Revision history for this message
Richard Harding (rharding) wrote :

On 2013/09/13 13:24:32, frankban wrote:
> On 2013/09/13 13:16:21, rharding wrote:

> > server/guiserver/bundles/views.py:182: deployment_id =
> > request.params.get('DeploymentId')
> > 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.

https://codereview.appspot.com/13549046/

Revision history for this message
Richard Harding (rharding) wrote :

LGTM

Results of the script, first run:

http://paste.mitechie.com/show/1020/

I did run it a second time and got an error:

http://paste.mitechie.com/show/1021/

I'm not sure if this is a bug here or as a follow up to not crash when
the service is already there.

https://codereview.appspot.com/13549046/

117. By Francesco Banconi

Debugging again.

Revision history for this message
Francesco Banconi (frankban) wrote :

On 2013/09/13 13:45:00, rharding wrote:
> LGTM

> Results of the script, first run:

> http://paste.mitechie.com/show/1020/

Great, thank you for QAing this branch!

> I did run it a second time and got an error:

> http://paste.mitechie.com/show/1021/

> 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!

https://codereview.appspot.com/13549046/

118. By Francesco Banconi

Final debugging.

Revision history for this message
Brad Crittenden (bac) wrote :

LGTM

https://codereview.appspot.com/13549046/diff/1/server/guiserver/bundles/base.py
File server/guiserver/bundles/base.py (right):

https://codereview.appspot.com/13549046/diff/1/server/guiserver/bundles/base.py#newcode152
server/guiserver/bundles/base.py:152: # cancelling scheduled jobs, even
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.

https://codereview.appspot.com/13549046/

Revision history for this message
Francesco Banconi (frankban) wrote :
Download full text (3.2 KiB)

*** 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-server=true`;
- ensure the GUI is working well by visiting
   https://GUI-ADDRESS .

To test the deployer support and the
"cancel deployment" feature, use the script in
http://pastebin.ubuntu.com/6100815/ e.g.:

- download and save the Python script;
- run it passing the GUI node address as first argument:
   `python start-deployer-cancel.py GUI-ADDRESS`.

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://pastebin.ubuntu.com/6100861/ ;
- 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://GUI-ADDRESS/gui-server-info you
   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://ec2-50-17-116-51.compute-1.amazonaws.com:17070"
   }

That's all, thanks a lot for QAing this branch!

R=rharding, bac
CC=
https://codereview.appspot.com/13549046

https://codereview.appspot.com/13549046/diff/1/server/guiserver/bundles/base.py
File server/guiserver/bundles/base.py (right):

https://codereview.appspot.com/13549046/diff/1/server/guiserver/bundles/base.py#newcode152
server/guiserver/bundles/base.py:152: # cancelling scheduled jobs, even
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...

Read more...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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):

Subscribers

People subscribed via source and target branches