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
=== modified file 'revision'
--- revision 2013-09-12 19:58:07 +0000
+++ revision 2013-09-13 14:09:53 +0000
@@ -1,1 +1,1 @@
184185
22
=== modified file 'server/guiserver/__init__.py'
--- server/guiserver/__init__.py 2013-08-23 14:44:00 +0000
+++ server/guiserver/__init__.py 2013-09-13 14:09:53 +0000
@@ -30,7 +30,7 @@
30the HTTPS connection, allowing changes in the Juju environment to be propagated30the HTTPS connection, allowing changes in the Juju environment to be propagated
31and shown immediately by the browser. """31and shown immediately by the browser. """
3232
33VERSION = (0, 1, 0)33VERSION = (0, 2, 0)
3434
3535
36def get_version():36def get_version():
3737
=== modified file 'server/guiserver/bundles/__init__.py'
--- server/guiserver/bundles/__init__.py 2013-09-04 15:21:54 +0000
+++ server/guiserver/bundles/__init__.py 2013-09-13 14:09:53 +0000
@@ -16,9 +16,6 @@
1616
17"""Juju GUI server bundles support.17"""Juju GUI server bundles support.
1818
19XXX frankban: note that the following is a work in progress. Some of the
20objects described below are not yet implemented.
21
22This package includes the objects and functions required to support deploying19This package includes the objects and functions required to support deploying
23bundles in juju-core. The base pieces of the infrastructure are placed in the20bundles in juju-core. The base pieces of the infrastructure are placed in the
24base module:21base module:
@@ -195,7 +192,9 @@
195bundle at the time. A Queue value of zero means the deployment will be started192bundle at the time. A Queue value of zero means the deployment will be started
196as soon as possible.193as soon as possible.
197194
198The Status can be one of the following: 'scheduled', 'started' and 'completed'.195The Status can be one of the following: 'scheduled', 'started', 'completed' and
196'cancelled. See the next section for an explanation of how to cancel a pending
197(scheduled) deployment.
199198
200The Time field indicates the number of seconds since the epoch at the time of199The Time field indicates the number of seconds since the epoch at the time of
201the change.200the change.
@@ -222,6 +221,40 @@
222XXX frankban: a timeout to delete completed deployments history will be221XXX frankban: a timeout to delete completed deployments history will be
223eventually implemented.222eventually implemented.
224223
224Cancelling a deployment.
225------------------------
226
227It is possible to cancel the execution of scheduled deployments by sending a
228Cancel request, e.g.:
229
230 {
231 'RequestId': 5,
232 'Type': 'Deployer',
233 'Request': 'Cancel',
234 'Params': {'DeploymentId': 42},
235 }
236
237Note that it is allowed to cancel a deployment only if it is not yet started,
238i.e. if it is in a 'scheduled' state.
239
240If any error occurs, the response is like this:
241
242 {
243 'RequestId': 5,
244 'Response': {},
245 'Error': 'some error: error details',
246 }
247
248Usually an error response is returned when either an invalid deployment id was
249provided or the request attempted to cancel an already started deployment.
250
251If the deployment is successfully cancelled, the response is the following:
252
253 {
254 'RequestId': 5,
255 'Response': {},
256 }
257
225Deployments status.258Deployments status.
226-------------------259-------------------
227260
@@ -229,7 +262,7 @@
229the client can send the following request:262the client can send the following request:
230263
231 {264 {
232 'RequestId': 5,265 'RequestId': 6,
233 'Type': 'Deployer',266 'Type': 'Deployer',
234 'Request': 'Status',267 'Request': 'Status',
235 }268 }
@@ -238,29 +271,29 @@
238the second one is a successful response:271the second one is a successful response:
239272
240 {273 {
241 'RequestId': 5,274 'RequestId': 6,
242 'Response': {},275 'Response': {},
243 'Error': 'some error: error details',276 'Error': 'some error: error details',
244 }277 }
245278
246 {279 {
247 'RequestId': 5,280 'RequestId': 6,
248 'Response': {281 'Response': {
249 'LastChanges': [282 'LastChanges': [
250 {'DeploymentId': 42, 'Status': 'completed', 'Time': 1377080001,283 {'DeploymentId': 1, 'Status': 'completed', 'Time': 1377080001,
251 'Error': 'error'},284 'Error': 'error'},
252 {'DeploymentId': 43, 'Status': 'completed',285 {'DeploymentId': 2, 'Status': 'completed', 'Time': 1377080002},
253 'Time': 1377080002},286 {'DeploymentId': 3, 'Status': 'started', 'Time': 1377080003,
254 {'DeploymentId': 44, 'Status': 'started', 'Time': 1377080003,
255 'Queue': 0},287 'Queue': 0},
256 {'DeploymentId': 45, 'Status': 'scheduled', 'Time': 1377080004,288 {'DeploymentId': 4, 'Status': 'cancelled', 'Time': 1377080004},
289 {'DeploymentId': 5, 'Status': 'scheduled', 'Time': 1377080005,
257 'Queue': 1},290 'Queue': 1},
258 ],291 ],
259 },292 },
260 }293 }
261294
262In the second response above, the Error field in the first attempted deployment295In the second response above, the Error field in the first attempted deployment
263(42) contains details about an error that occurred while deploying a bundle.296(1) contains details about an error that occurred while deploying a bundle.
264This means that bundle deployment has been completed but an error occurred297This means that bundle deployment has been completed but an error occurred
265during the process.298during the process.
266"""299"""
267300
=== modified file 'server/guiserver/bundles/base.py'
--- server/guiserver/bundles/base.py 2013-08-26 07:56:52 +0000
+++ server/guiserver/bundles/base.py 2013-09-13 14:09:53 +0000
@@ -23,7 +23,12 @@
23a detailed explanation of how these objects are used.23a detailed explanation of how these objects are used.
24"""24"""
2525
26from concurrent.futures import ProcessPoolExecutor26import time
27
28from concurrent.futures import (
29 process,
30 ProcessPoolExecutor,
31)
27from tornado import gen32from tornado import gen
28from tornado.ioloop import IOLoop33from tornado.ioloop import IOLoop
29from tornado.util import ObjectDict34from tornado.util import ObjectDict
@@ -37,6 +42,10 @@
37from guiserver.watchers import WatcherError42from guiserver.watchers import WatcherError
3843
3944
45# Controls how many more calls than processes will be queued in the call queue.
46# Set to zero to make Future.cancel() succeed more frequently (Futures in the
47# call queue cannot be cancelled).
48process.EXTRA_QUEUED_CALLS = 0
40# Juju API versions supported by the GUI server Deployer.49# Juju API versions supported by the GUI server Deployer.
41# Tests use the first API version in this list.50# Tests use the first API version in this list.
42SUPPORTED_API_VERSIONS = ['go']51SUPPORTED_API_VERSIONS = ['go']
@@ -78,6 +87,8 @@
78 # Queue stores the deployment identifiers corresponding to the87 # Queue stores the deployment identifiers corresponding to the
79 # currently started/queued jobs.88 # currently started/queued jobs.
80 self._queue = []89 self._queue = []
90 # The futures attribute maps deployment identifiers to Futures.
91 self._futures = {}
8192
82 @gen.coroutine93 @gen.coroutine
83 def validate(self, user, name, bundle):94 def validate(self, user, name, bundle):
@@ -132,9 +143,14 @@
132 future = self._run_executor.submit(143 future = self._run_executor.submit(
133 blocking.import_bundle, self._apiurl, user.password, name, bundle)144 blocking.import_bundle, self._apiurl, user.password, name, bundle)
134 add_future(self._io_loop, future, self._import_callback, deployment_id)145 add_future(self._io_loop, future, self._import_callback, deployment_id)
146 self._futures[deployment_id] = future
135 # If a customized callback is provided, schedule it as well.147 # If a customized callback is provided, schedule it as well.
136 if test_callback is not None:148 if test_callback is not None:
137 add_future(self._io_loop, future, test_callback)149 add_future(self._io_loop, future, test_callback)
150 # Submit a sleeping job in order to avoid the next deployment job to be
151 # immediately put in the executor's call queue. This allows for
152 # cancelling scheduled jobs, even if the job is the next to be started.
153 self._run_executor.submit(time.sleep, 1)
138 return deployment_id154 return deployment_id
139155
140 def _import_callback(self, deployment_id, future):156 def _import_callback(self, deployment_id, future):
@@ -144,12 +160,17 @@
144 deployment_id identifying one specific deployment job, and the fired160 deployment_id identifying one specific deployment job, and the fired
145 future returned by the executor.161 future returned by the executor.
146 """162 """
147 exception = future.exception()163 if future.cancelled():
148 error = None if exception is None else str(exception)164 # Notify a deployment has been cancelled.
149 # Notify a deployment completed.165 self._observer.notify_cancelled(deployment_id)
150 self._observer.notify_completed(deployment_id, error=error)166 else:
167 exception = future.exception()
168 error = None if exception is None else str(exception)
169 # Notify a deployment completed.
170 self._observer.notify_completed(deployment_id, error=error)
151 # Remove the completed deployment job from the queue.171 # Remove the completed deployment job from the queue.
152 self._queue.remove(deployment_id)172 self._queue.remove(deployment_id)
173 del self._futures[deployment_id]
153 # Notify the new position of all remaining deployments in the queue.174 # Notify the new position of all remaining deployments in the queue.
154 for position, deploy_id in enumerate(self._queue):175 for position, deploy_id in enumerate(self._queue):
155 self._observer.notify_position(deploy_id, position)176 self._observer.notify_position(deploy_id, position)
@@ -183,6 +204,18 @@
183 except WatcherError:204 except WatcherError:
184 return205 return
185206
207 def cancel(self, deployment_id):
208 """Attempt to cancel the deployment identified by deployment_id.
209
210 Return None if the deployment has been correctly cancelled.
211 Return an error string otherwise.
212 """
213 future = self._futures.get(deployment_id)
214 if future is None:
215 return 'deployment not found or already completed'
216 if not future.cancel():
217 return 'unable to cancel the deployment'
218
186 def status(self):219 def status(self):
187 """Return a list containing the last known change for each deployment.220 """Return a list containing the last known change for each deployment.
188 """221 """
@@ -221,6 +254,7 @@
221 'Import': views.import_bundle,254 'Import': views.import_bundle,
222 'Watch': views.watch,255 'Watch': views.watch,
223 'Next': views.next,256 'Next': views.next,
257 'Cancel': views.cancel,
224 'Status': views.status,258 'Status': views.status,
225 }259 }
226260
227261
=== modified file 'server/guiserver/bundles/utils.py'
--- server/guiserver/bundles/utils.py 2013-08-21 16:12:45 +0000
+++ server/guiserver/bundles/utils.py 2013-09-13 14:09:53 +0000
@@ -29,6 +29,7 @@
29# Change statuses.29# Change statuses.
30SCHEDULED = 'scheduled'30SCHEDULED = 'scheduled'
31STARTED = 'started'31STARTED = 'started'
32CANCELLED = 'cancelled'
32COMPLETED = 'completed'33COMPLETED = 'completed'
3334
3435
@@ -99,6 +100,12 @@
99 change = create_change(deployment_id, status, queue=position)100 change = create_change(deployment_id, status, queue=position)
100 watcher.put(change)101 watcher.put(change)
101102
103 def notify_cancelled(self, deployment_id):
104 """Add a change to the deployment watcher notifying it is cancelled."""
105 watcher = self.deployments[deployment_id]
106 change = create_change(deployment_id, CANCELLED)
107 watcher.close(change)
108
102 def notify_completed(self, deployment_id, error=None):109 def notify_completed(self, deployment_id, error=None):
103 """Add a change to the deployment watcher notifying it is completed."""110 """Add a change to the deployment watcher notifying it is completed."""
104 watcher = self.deployments[deployment_id]111 watcher = self.deployments[deployment_id]
105112
=== modified file 'server/guiserver/bundles/views.py'
--- server/guiserver/bundles/views.py 2013-09-04 15:22:42 +0000
+++ server/guiserver/bundles/views.py 2013-09-13 14:09:53 +0000
@@ -169,6 +169,28 @@
169169
170@gen.coroutine170@gen.coroutine
171@require_authenticated_user171@require_authenticated_user
172def cancel(request, deployer):
173 """Cancel the given pending deployment.
174
175 The deployment is identified in the request by the DeploymentId parameter.
176 If the request is not valid or the deployment cannot be cancelled (e.g.
177 because it is already started) an error response is returned.
178
179 Request: 'Cancel'.
180 Parameters example: {'DeploymentId': 42}.
181 """
182 deployment_id = request.params.get('DeploymentId')
183 if deployment_id is None:
184 raise response(error='invalid request: invalid data parameters')
185 # Use the Deployer instance to cancel the deployment.
186 err = deployer.cancel(deployment_id)
187 if err is not None:
188 raise response(error='invalid request: {}'.format(err))
189 raise response()
190
191
192@gen.coroutine
193@require_authenticated_user
172def status(request, deployer):194def status(request, deployer):
173 """Return the current status of all the bundle deployments.195 """Return the current status of all the bundle deployments.
174196
175197
=== modified file 'server/guiserver/tests/bundles/test_base.py'
--- server/guiserver/tests/bundles/test_base.py 2013-08-26 07:56:52 +0000
+++ server/guiserver/tests/bundles/test_base.py 2013-09-13 14:09:53 +0000
@@ -195,13 +195,75 @@
195 # Wait for the deployment to be completed.195 # Wait for the deployment to be completed.
196 self.wait()196 self.wait()
197197
198 @gen_test
199 def test_invalid_watcher(self):198 def test_invalid_watcher(self):
200 # None is returned if the watcher id is not valid.199 # None is returned if the watcher id is not valid.
201 deployer = self.make_deployer()200 deployer = self.make_deployer()
202 changes = deployer.next(42)201 changes = deployer.next(42)
203 self.assertIsNone(changes)202 self.assertIsNone(changes)
204203
204 @gen_test
205 def test_cancel(self):
206 # It is possible to cancel the execution of a pending deployment.
207 deployer = self.make_deployer()
208 with self.patch_import_bundle():
209 # The test callback is passed to the first deployment because we
210 # expect the second one to be immediately cancelled.
211 deployer.import_bundle(
212 self.user, 'bundle', self.bundle, test_callback=self.stop)
213 deployment_id = deployer.import_bundle(
214 self.user, 'bundle', self.bundle)
215 watcher_id = deployer.watch(deployment_id)
216 self.assertIsNone(deployer.cancel(deployment_id))
217 # We expect two changes: the second one should notify the deployment
218 # has been cancelled.
219 yield deployer.next(watcher_id)
220 changes = yield deployer.next(watcher_id)
221 self.assert_change(changes, deployment_id, utils.CANCELLED)
222 # Wait for the deployment to be completed.
223 self.wait()
224
225 def test_cancel_unknown_deployment(self):
226 # An error is returned when trying to cancel an invalid deployment.
227 deployer = self.make_deployer()
228 error = deployer.cancel(42)
229 self.assertEqual('deployment not found or already completed', error)
230
231 @gen_test
232 def test_cancel_completed_deployment(self):
233 # An error is returned when trying to cancel a completed deployment.
234 deployer = self.make_deployer()
235 with self.patch_import_bundle():
236 deployment_id = deployer.import_bundle(
237 self.user, 'bundle', self.bundle, test_callback=self.stop)
238 watcher_id = deployer.watch(deployment_id)
239 # Assume the deployment is completed after two changes.
240 yield deployer.next(watcher_id)
241 yield deployer.next(watcher_id)
242 error = deployer.cancel(deployment_id)
243 self.assertEqual('deployment not found or already completed', error)
244 # Wait for the deployment to be completed.
245 self.wait()
246
247 @gen_test
248 def test_cancel_started_deployment(self):
249 # An error is returned when trying to cancel a deployment already
250 # started.
251 deployer = self.make_deployer()
252 with self.patch_import_bundle() as mock_import_bundle:
253 deployment_id = deployer.import_bundle(
254 self.user, 'bundle', self.bundle, test_callback=self.stop)
255 watcher_id = deployer.watch(deployment_id)
256 # Wait until the deployment is started.
257 yield deployer.next(watcher_id)
258 while True:
259 if mock_import_bundle.call_count:
260 break
261 error = deployer.cancel(deployment_id)
262 self.assertEqual('unable to cancel the deployment', error)
263 # Wait for the deployment to be completed.
264 yield deployer.next(watcher_id)
265 self.wait()
266
205 def test_initial_status(self):267 def test_initial_status(self):
206 # The initial deployer status is an empty list.268 # The initial deployer status is an empty list.
207 deployer = self.make_deployer()269 deployer = self.make_deployer()
208270
=== modified file 'server/guiserver/tests/bundles/test_utils.py'
--- server/guiserver/tests/bundles/test_utils.py 2013-08-21 16:12:45 +0000
+++ server/guiserver/tests/bundles/test_utils.py 2013-09-13 14:09:53 +0000
@@ -32,12 +32,15 @@
32from guiserver.tests import helpers32from guiserver.tests import helpers
3333
3434
35@mock.patch('time.time', mock.Mock(return_value=1234))35mock_time = mock.patch('time.time', mock.Mock(return_value=12345))
36
37
38@mock_time
36class TestCreateChange(unittest.TestCase):39class TestCreateChange(unittest.TestCase):
3740
38 def test_status(self):41 def test_status(self):
39 # The change includes the deployment status.42 # The change includes the deployment status.
40 expected = {'DeploymentId': 0, 'Status': utils.STARTED, 'Time': 1234}43 expected = {'DeploymentId': 0, 'Status': utils.STARTED, 'Time': 12345}
41 obtained = utils.create_change(0, utils.STARTED)44 obtained = utils.create_change(0, utils.STARTED)
42 self.assertEqual(expected, obtained)45 self.assertEqual(expected, obtained)
4346
@@ -46,7 +49,7 @@
46 expected = {49 expected = {
47 'DeploymentId': 1,50 'DeploymentId': 1,
48 'Status': utils.SCHEDULED,51 'Status': utils.SCHEDULED,
49 'Time': 1234,52 'Time': 12345,
50 'Queue': 42,53 'Queue': 42,
51 }54 }
52 obtained = utils.create_change(1, utils.SCHEDULED, queue=42)55 obtained = utils.create_change(1, utils.SCHEDULED, queue=42)
@@ -57,7 +60,7 @@
57 expected = {60 expected = {
58 'DeploymentId': 2,61 'DeploymentId': 2,
59 'Status': utils.COMPLETED,62 'Status': utils.COMPLETED,
60 'Time': 1234,63 'Time': 12345,
61 'Error': 'an error',64 'Error': 'an error',
62 }65 }
63 obtained = utils.create_change(2, utils.COMPLETED, error='an error')66 obtained = utils.create_change(2, utils.COMPLETED, error='an error')
@@ -68,7 +71,7 @@
68 expected = {71 expected = {
69 'DeploymentId': 3,72 'DeploymentId': 3,
70 'Status': utils.COMPLETED,73 'Status': utils.COMPLETED,
71 'Time': 1234,74 'Time': 12345,
72 'Queue': 47,75 'Queue': 47,
73 'Error': 'an error',76 'Error': 'an error',
74 }77 }
@@ -139,7 +142,7 @@
139 self.assert_watcher(watcher1, deployment1)142 self.assert_watcher(watcher1, deployment1)
140 self.assert_watcher(watcher2, deployment2)143 self.assert_watcher(watcher2, deployment2)
141144
142 @mock.patch('time.time', mock.Mock(return_value=1234))145 @mock_time
143 def test_notify_scheduled(self):146 def test_notify_scheduled(self):
144 # It is possible to notify a new queue position for a deployment.147 # It is possible to notify a new queue position for a deployment.
145 deployment_id = self.observer.add_deployment()148 deployment_id = self.observer.add_deployment()
@@ -148,13 +151,13 @@
148 expected = {151 expected = {
149 'DeploymentId': deployment_id,152 'DeploymentId': deployment_id,
150 'Status': utils.SCHEDULED,153 'Status': utils.SCHEDULED,
151 'Time': 1234,154 'Time': 12345,
152 'Queue': 3,155 'Queue': 3,
153 }156 }
154 self.assertEqual(expected, watcher.getlast())157 self.assertEqual(expected, watcher.getlast())
155 self.assertFalse(watcher.closed)158 self.assertFalse(watcher.closed)
156159
157 @mock.patch('time.time', mock.Mock(return_value=12345))160 @mock_time
158 def test_notify_started(self):161 def test_notify_started(self):
159 # It is possible to notify that a deployment is (about to be) started.162 # It is possible to notify that a deployment is (about to be) started.
160 deployment_id = self.observer.add_deployment()163 deployment_id = self.observer.add_deployment()
@@ -169,7 +172,21 @@
169 self.assertEqual(expected, watcher.getlast())172 self.assertEqual(expected, watcher.getlast())
170 self.assertFalse(watcher.closed)173 self.assertFalse(watcher.closed)
171174
172 @mock.patch('time.time', mock.Mock(return_value=123456))175 @mock_time
176 def test_notify_cancelled(self):
177 # It is possible to notify that a deployment has been cancelled.
178 deployment_id = self.observer.add_deployment()
179 watcher = self.observer.deployments[deployment_id]
180 self.observer.notify_cancelled(deployment_id)
181 expected = {
182 'DeploymentId': deployment_id,
183 'Status': utils.CANCELLED,
184 'Time': 12345,
185 }
186 self.assertEqual(expected, watcher.getlast())
187 self.assertTrue(watcher.closed)
188
189 @mock_time
173 def test_notify_completed(self):190 def test_notify_completed(self):
174 # It is possible to notify that a deployment is completed.191 # It is possible to notify that a deployment is completed.
175 deployment_id = self.observer.add_deployment()192 deployment_id = self.observer.add_deployment()
@@ -178,12 +195,12 @@
178 expected = {195 expected = {
179 'DeploymentId': deployment_id,196 'DeploymentId': deployment_id,
180 'Status': utils.COMPLETED,197 'Status': utils.COMPLETED,
181 'Time': 123456,198 'Time': 12345,
182 }199 }
183 self.assertEqual(expected, watcher.getlast())200 self.assertEqual(expected, watcher.getlast())
184 self.assertTrue(watcher.closed)201 self.assertTrue(watcher.closed)
185202
186 @mock.patch('time.time', mock.Mock(return_value=1234567))203 @mock_time
187 def test_notify_error(self):204 def test_notify_error(self):
188 # It is possible to notify that an error occurred during a deployment.205 # It is possible to notify that an error occurred during a deployment.
189 deployment_id = self.observer.add_deployment()206 deployment_id = self.observer.add_deployment()
@@ -192,7 +209,7 @@
192 expected = {209 expected = {
193 'DeploymentId': deployment_id,210 'DeploymentId': deployment_id,
194 'Status': utils.COMPLETED,211 'Status': utils.COMPLETED,
195 'Time': 1234567,212 'Time': 12345,
196 'Error': 'bad wolf',213 'Error': 'bad wolf',
197 }214 }
198 self.assertEqual(expected, watcher.getlast())215 self.assertEqual(expected, watcher.getlast())
199216
=== modified file 'server/guiserver/tests/bundles/test_views.py'
--- server/guiserver/tests/bundles/test_views.py 2013-09-04 10:59:44 +0000
+++ server/guiserver/tests/bundles/test_views.py 2013-09-13 14:09:53 +0000
@@ -267,6 +267,43 @@
267 self.deployer.next.assert_called_once_with(42)267 self.deployer.next.assert_called_once_with(42)
268268
269269
270class TestCancel(
271 ViewsTestMixin, helpers.BundlesTestMixin, LogTrapTestCase,
272 AsyncTestCase):
273
274 def get_view(self):
275 return views.cancel
276
277 @gen_test
278 def test_invalid_deployment(self):
279 # An error response is returned if the deployment identifier is not
280 # valid.
281 request = self.make_view_request(params={'DeploymentId': 42})
282 # Set up the Deployer mock.
283 self.deployer.cancel.return_value = 'bad wolf'
284 # Execute the view.
285 response = yield self.view(request, self.deployer)
286 expected_response = {
287 'Response': {},
288 'Error': 'invalid request: bad wolf',
289 }
290 self.assertEqual(expected_response, response)
291 # Ensure the Deployer methods have been correctly called.
292 self.deployer.cancel.assert_called_once_with(42)
293
294 @gen_test
295 def test_success(self):
296 # An empty response is returned if everything is ok.
297 request = self.make_view_request(params={'DeploymentId': 42})
298 # Set up the Deployer mock.
299 self.deployer.cancel.return_value = None
300 # Execute the view.
301 response = yield self.view(request, self.deployer)
302 self.assertEqual({'Response': {}}, response)
303 # Ensure the Deployer methods have been correctly called.
304 self.deployer.cancel.assert_called_once_with(42)
305
306
270class TestStatus(307class TestStatus(
271 ViewsTestMixin, helpers.BundlesTestMixin, LogTrapTestCase,308 ViewsTestMixin, helpers.BundlesTestMixin, LogTrapTestCase,
272 AsyncTestCase):309 AsyncTestCase):

Subscribers

People subscribed via source and target branches