Merge lp:~frankban/charms/precise/juju-gui/guiserver-bundles-base-helpers into lp:~juju-gui/charms/precise/juju-gui/trunk
- Precise Pangolin (12.04)
- guiserver-bundles-base-helpers
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 94 |
Proposed branch: | lp:~frankban/charms/precise/juju-gui/guiserver-bundles-base-helpers |
Merge into: | lp:~juju-gui/charms/precise/juju-gui/trunk |
Diff against target: |
537 lines (+399/-11) 6 files modified
revision (+1/-1) server/guiserver/bundles/__init__.py (+18/-7) server/guiserver/bundles/utils.py (+85/-1) server/guiserver/tests/bundles/test_utils.py (+170/-1) server/guiserver/tests/test_utils.py (+98/-1) server/guiserver/utils.py (+27/-0) |
To merge this branch: | bzr merge lp:~frankban/charms/precise/juju-gui/guiserver-bundles-base-helpers |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
charmers | Pending | ||
Review via email: mp+181269@code.launchpad.net |
Commit message
Description of the change
GUI server: base deployer helpers.
This branch includes some helper functions
and objects that are required to implement
the Deployer and DeployMiddleware classes.
To avoid this branch to include too many
changes, the base classes above will be
implemented in a separate branch.
Tests: `make unittest` from the branch root.
Francesco Banconi (frankban) wrote : | # |
Brad Crittenden (bac) wrote : | # |
LGTM, thanks.
https:/
File server/
https:/
server/
the epoch as an int.
Would be nice to document the optional fields.
https:/
File server/
https:/
server/
test_notify_
A test exercising showing the STARTED state would be nice.
- 118. By Francesco Banconi
-
Changes as per review.
Francesco Banconi (frankban) wrote : | # |
*** Submitted:
GUI server: base deployer helpers.
This branch includes some helper functions
and objects that are required to implement
the Deployer and DeployMiddleware classes.
To avoid this branch to include too many
changes, the base classes above will be
implemented in a separate branch.
Tests: `make unittest` from the branch root.
R=benji, bac
CC=
https:/
https:/
File server/
https:/
server/
the epoch as an int.
On 2013/08/21 14:19:18, bac wrote:
> Would be nice to document the optional fields.
Done.
https:/
server/
watcher notifying a new position."""
On 2013/08/21 13:18:10, benji wrote:
> I think I see that "position" is the position of the deployment in the
queue but
> I don't quite see how bool(position) == False implies that the
deployment has
> been started. Maybe some more text in a comment the docstring would
help.
Done.
https:/
File server/
https:/
server/
test_notify_
On 2013/08/21 14:19:18, bac wrote:
> A test exercising showing the STARTED state would be nice.
You are right! Missed it, thank you.
Francesco Banconi (frankban) wrote : | # |
Thanks for the reviews!
Preview Diff
1 | === modified file 'revision' |
2 | --- revision 2013-08-20 08:02:05 +0000 |
3 | +++ revision 2013-08-21 16:12:55 +0000 |
4 | @@ -1,1 +1,1 @@ |
5 | -72 |
6 | +73 |
7 | |
8 | === modified file 'server/guiserver/bundles/__init__.py' |
9 | --- server/guiserver/bundles/__init__.py 2013-08-20 08:00:13 +0000 |
10 | +++ server/guiserver/bundles/__init__.py 2013-08-21 16:12:55 +0000 |
11 | @@ -178,9 +178,12 @@ |
12 | 'RequestId': 3, |
13 | 'Response': { |
14 | 'Changes': [ |
15 | - {'DeploymentId': 42, 'Status': 'scheduled', 'Queue': 2}, |
16 | - {'DeploymentId': 42, 'Status': 'scheduled', 'Queue': 1}, |
17 | - {'DeploymentId': 42, 'Status': 'started', 'Queue': 0}, |
18 | + {'DeploymentId': 42, 'Status': 'scheduled', 'Time': 1377080066, |
19 | + 'Queue': 2}, |
20 | + {'DeploymentId': 42, 'Status': 'scheduled', 'Time': 1377080062, |
21 | + 'Queue': 1}, |
22 | + {'DeploymentId': 42, 'Status': 'started', 'Time': 1377080000, |
23 | + 'Queue': 0}, |
24 | ], |
25 | }, |
26 | } |
27 | @@ -192,6 +195,9 @@ |
28 | |
29 | The Status can be one of the following: 'scheduled', 'started' and 'completed'. |
30 | |
31 | +The Time field indicates the number of seconds since the epoch at the time of |
32 | +the change. |
33 | + |
34 | The Next request can be performed as many times as required by the API clients |
35 | after receiving a response from a previous one. However, if the Status of the |
36 | last deployment change is 'completed', no further changes will be notified, and |
37 | @@ -204,6 +210,7 @@ |
38 | { |
39 | 'DeploymentId': 42, |
40 | 'Status': 'completed', |
41 | + 'Time': 1377080000, |
42 | 'Error': 'this field is only present if an error occurred', |
43 | }, |
44 | ], |
45 | @@ -238,10 +245,14 @@ |
46 | 'RequestId': 5, |
47 | 'Response': { |
48 | 'LastChanges': [ |
49 | - {'DeploymentId': 42, 'Status': 'completed', 'Error': 'error'}, |
50 | - {'DeploymentId': 43, 'Status': 'completed'}, |
51 | - {'DeploymentId': 44, 'Status': 'started', 'Queue': 0}, |
52 | - {'DeploymentId': 45, 'Status': 'scheduled', 'Queue': 1}, |
53 | + {'DeploymentId': 42, 'Status': 'completed', 'Time': 1377080001, |
54 | + 'Error': 'error'}, |
55 | + {'DeploymentId': 43, 'Status': 'completed', |
56 | + 'Time': 1377080002}, |
57 | + {'DeploymentId': 44, 'Status': 'started', 'Time': 1377080003, |
58 | + 'Queue': 0}, |
59 | + {'DeploymentId': 45, 'Status': 'scheduled', 'Time': 1377080004, |
60 | + 'Queue': 1}, |
61 | ], |
62 | }, |
63 | } |
64 | |
65 | === modified file 'server/guiserver/bundles/utils.py' |
66 | --- server/guiserver/bundles/utils.py 2013-08-20 14:05:39 +0000 |
67 | +++ server/guiserver/bundles/utils.py 2013-08-21 16:12:55 +0000 |
68 | @@ -14,13 +14,97 @@ |
69 | # You should have received a copy of the GNU Affero General Public License |
70 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
71 | |
72 | -"""Bundle deployment utility functions.""" |
73 | +"""Bundle deployment utility functions and objects.""" |
74 | |
75 | from functools import wraps |
76 | +import itertools |
77 | import logging |
78 | +import time |
79 | |
80 | from tornado import gen |
81 | |
82 | +from guiserver.watchers import AsyncWatcher |
83 | + |
84 | + |
85 | +# Change statuses. |
86 | +SCHEDULED = 'scheduled' |
87 | +STARTED = 'started' |
88 | +COMPLETED = 'completed' |
89 | + |
90 | + |
91 | +def create_change(deployment_id, status, queue=None, error=None): |
92 | + """Return a dict representing a deployment change. |
93 | + |
94 | + The resulting dict contains at least the following fields: |
95 | + - DeploymentId: the deployment identifier; |
96 | + - Status: the deployment's current status; |
97 | + - Time: the time in seconds since the epoch as an int. |
98 | + |
99 | + These optional fields can also be present: |
100 | + - Queue: the deployment position in the queue at the time of this change; |
101 | + - Error: a message describing an error occurred during the deployment. |
102 | + """ |
103 | + result = { |
104 | + 'DeploymentId': deployment_id, |
105 | + 'Status': status, |
106 | + 'Time': int(time.time()), |
107 | + } |
108 | + if queue is not None: |
109 | + result['Queue'] = queue |
110 | + if error is not None: |
111 | + result['Error'] = error |
112 | + return result |
113 | + |
114 | + |
115 | +class Observer(object): |
116 | + """Handle multiple deployment watchers.""" |
117 | + |
118 | + def __init__(self): |
119 | + # Map deployment identifiers to watchers. |
120 | + self.deployments = {} |
121 | + # Map watcher identifiers to deployment identifiers. |
122 | + self.watchers = {} |
123 | + # This counter is used to generate deployment identifiers. |
124 | + self._deployment_counter = itertools.count() |
125 | + # This counter is used to generate watcher identifiers. |
126 | + self._watcher_counter = itertools.count() |
127 | + |
128 | + def add_deployment(self): |
129 | + """Start observing a deployment. |
130 | + |
131 | + Generate a deployment id and add it to self.deployments. |
132 | + Return the generated deployment id. |
133 | + """ |
134 | + deployment_id = self._deployment_counter.next() |
135 | + self.deployments[deployment_id] = AsyncWatcher() |
136 | + return deployment_id |
137 | + |
138 | + def add_watcher(self, deployment_id): |
139 | + """Return a new watcher id for the given deployment id. |
140 | + |
141 | + Also add the generated watcher id to self.watchers. |
142 | + """ |
143 | + watcher_id = self._watcher_counter.next() |
144 | + self.watchers[watcher_id] = deployment_id |
145 | + return watcher_id |
146 | + |
147 | + def notify_position(self, deployment_id, position): |
148 | + """Add a change to the deployment watcher notifying a new position. |
149 | + |
150 | + If the position in the queue is 0, it means the deployment is started |
151 | + or about to start. Therefore set its status to STARTED. |
152 | + """ |
153 | + watcher = self.deployments[deployment_id] |
154 | + status = SCHEDULED if position else STARTED |
155 | + change = create_change(deployment_id, status, queue=position) |
156 | + watcher.put(change) |
157 | + |
158 | + def notify_completed(self, deployment_id, error=None): |
159 | + """Add a change to the deployment watcher notifying it is completed.""" |
160 | + watcher = self.deployments[deployment_id] |
161 | + change = create_change(deployment_id, COMPLETED, error=error) |
162 | + watcher.close(change) |
163 | + |
164 | |
165 | def require_authenticated_user(view): |
166 | """Require the user to be authenticated when executing the decorated view. |
167 | |
168 | === modified file 'server/guiserver/tests/bundles/test_utils.py' |
169 | --- server/guiserver/tests/bundles/test_utils.py 2013-08-20 14:05:39 +0000 |
170 | +++ server/guiserver/tests/bundles/test_utils.py 2013-08-21 16:12:55 +0000 |
171 | @@ -14,10 +14,11 @@ |
172 | # You should have received a copy of the GNU Affero General Public License |
173 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
174 | |
175 | -"""Tests for the deployment utility functions.""" |
176 | +"""Tests for the deployment utility functions and objects.""" |
177 | |
178 | import unittest |
179 | |
180 | +import mock |
181 | from tornado import gen |
182 | from tornado.testing import( |
183 | AsyncTestCase, |
184 | @@ -26,10 +27,178 @@ |
185 | LogTrapTestCase, |
186 | ) |
187 | |
188 | +from guiserver import watchers |
189 | from guiserver.bundles import utils |
190 | from guiserver.tests import helpers |
191 | |
192 | |
193 | +@mock.patch('time.time', mock.Mock(return_value=1234)) |
194 | +class TestCreateChange(unittest.TestCase): |
195 | + |
196 | + def test_status(self): |
197 | + # The change includes the deployment status. |
198 | + expected = {'DeploymentId': 0, 'Status': utils.STARTED, 'Time': 1234} |
199 | + obtained = utils.create_change(0, utils.STARTED) |
200 | + self.assertEqual(expected, obtained) |
201 | + |
202 | + def test_queue(self): |
203 | + # The change includes the deployment queue length. |
204 | + expected = { |
205 | + 'DeploymentId': 1, |
206 | + 'Status': utils.SCHEDULED, |
207 | + 'Time': 1234, |
208 | + 'Queue': 42, |
209 | + } |
210 | + obtained = utils.create_change(1, utils.SCHEDULED, queue=42) |
211 | + self.assertEqual(expected, obtained) |
212 | + |
213 | + def test_error(self): |
214 | + # The change includes a deployment error. |
215 | + expected = { |
216 | + 'DeploymentId': 2, |
217 | + 'Status': utils.COMPLETED, |
218 | + 'Time': 1234, |
219 | + 'Error': 'an error', |
220 | + } |
221 | + obtained = utils.create_change(2, utils.COMPLETED, error='an error') |
222 | + self.assertEqual(expected, obtained) |
223 | + |
224 | + def test_all_params(self): |
225 | + # The change includes all the parameters. |
226 | + expected = { |
227 | + 'DeploymentId': 3, |
228 | + 'Status': utils.COMPLETED, |
229 | + 'Time': 1234, |
230 | + 'Queue': 47, |
231 | + 'Error': 'an error', |
232 | + } |
233 | + obtained = utils.create_change( |
234 | + 3, utils.COMPLETED, queue=47, error='an error') |
235 | + self.assertEqual(expected, obtained) |
236 | + |
237 | + |
238 | +class TestObserver(unittest.TestCase): |
239 | + |
240 | + def setUp(self): |
241 | + self.observer = utils.Observer() |
242 | + |
243 | + def assert_deployment(self, deployment_id): |
244 | + """Ensure the given deployment id is being observed. |
245 | + |
246 | + Also check that a watcher is associated with the given deployment id. |
247 | + Return the watcher. |
248 | + """ |
249 | + deployments = self.observer.deployments |
250 | + self.assertIn(deployment_id, deployments) |
251 | + watcher = deployments[deployment_id] |
252 | + self.assertIsInstance(watcher, watchers.AsyncWatcher) |
253 | + return watcher |
254 | + |
255 | + def assert_watcher(self, watcher_id, deployment_id): |
256 | + """Ensure the given watcher id is associated with the deployment id.""" |
257 | + watchers = self.observer.watchers |
258 | + self.assertIn(watcher_id, watchers) |
259 | + self.assertEqual(deployment_id, watchers[watcher_id]) |
260 | + |
261 | + def test_initial(self): |
262 | + # A newly created observer does not contain deployments. |
263 | + self.assertEqual({}, self.observer.deployments) |
264 | + self.assertEqual({}, self.observer.watchers) |
265 | + |
266 | + def test_add_deployment(self): |
267 | + # A new deployment is correctly added to the observer. |
268 | + deployment_id = self.observer.add_deployment() |
269 | + self.assertEqual(1, len(self.observer.deployments)) |
270 | + self.assert_deployment(deployment_id) |
271 | + |
272 | + def test_add_multiple_deployments(self): |
273 | + # Multiple deployments can be added to the observer. |
274 | + deployment1 = self.observer.add_deployment() |
275 | + deployment2 = self.observer.add_deployment() |
276 | + self.assertNotEqual(deployment1, deployment2) |
277 | + self.assertEqual(2, len(self.observer.deployments)) |
278 | + watcher1 = self.assert_deployment(deployment1) |
279 | + watcher2 = self.assert_deployment(deployment2) |
280 | + self.assertNotEqual(watcher1, watcher2) |
281 | + |
282 | + def test_add_watcher(self): |
283 | + # A new watcher is correctly added to the observer. |
284 | + deployment_id = self.observer.add_deployment() |
285 | + watcher_id = self.observer.add_watcher(deployment_id) |
286 | + self.assertEqual(1, len(self.observer.watchers)) |
287 | + self.assert_watcher(watcher_id, deployment_id) |
288 | + |
289 | + def test_add_multiple_watchers(self): |
290 | + # Multiple watchers can be added to the observer. |
291 | + deployment1 = self.observer.add_deployment() |
292 | + deployment2 = self.observer.add_deployment() |
293 | + watcher1 = self.observer.add_watcher(deployment1) |
294 | + watcher2 = self.observer.add_watcher(deployment2) |
295 | + self.assertNotEqual(watcher1, watcher2) |
296 | + self.assertEqual(2, len(self.observer.watchers)) |
297 | + self.assert_watcher(watcher1, deployment1) |
298 | + self.assert_watcher(watcher2, deployment2) |
299 | + |
300 | + @mock.patch('time.time', mock.Mock(return_value=1234)) |
301 | + def test_notify_scheduled(self): |
302 | + # It is possible to notify a new queue position for a deployment. |
303 | + deployment_id = self.observer.add_deployment() |
304 | + watcher = self.observer.deployments[deployment_id] |
305 | + self.observer.notify_position(deployment_id, 3) |
306 | + expected = { |
307 | + 'DeploymentId': deployment_id, |
308 | + 'Status': utils.SCHEDULED, |
309 | + 'Time': 1234, |
310 | + 'Queue': 3, |
311 | + } |
312 | + self.assertEqual(expected, watcher.getlast()) |
313 | + self.assertFalse(watcher.closed) |
314 | + |
315 | + @mock.patch('time.time', mock.Mock(return_value=12345)) |
316 | + def test_notify_started(self): |
317 | + # It is possible to notify that a deployment is (about to be) started. |
318 | + deployment_id = self.observer.add_deployment() |
319 | + watcher = self.observer.deployments[deployment_id] |
320 | + self.observer.notify_position(deployment_id, 0) |
321 | + expected = { |
322 | + 'DeploymentId': deployment_id, |
323 | + 'Status': utils.STARTED, |
324 | + 'Time': 12345, |
325 | + 'Queue': 0, |
326 | + } |
327 | + self.assertEqual(expected, watcher.getlast()) |
328 | + self.assertFalse(watcher.closed) |
329 | + |
330 | + @mock.patch('time.time', mock.Mock(return_value=123456)) |
331 | + def test_notify_completed(self): |
332 | + # It is possible to notify that a deployment is completed. |
333 | + deployment_id = self.observer.add_deployment() |
334 | + watcher = self.observer.deployments[deployment_id] |
335 | + self.observer.notify_completed(deployment_id) |
336 | + expected = { |
337 | + 'DeploymentId': deployment_id, |
338 | + 'Status': utils.COMPLETED, |
339 | + 'Time': 123456, |
340 | + } |
341 | + self.assertEqual(expected, watcher.getlast()) |
342 | + self.assertTrue(watcher.closed) |
343 | + |
344 | + @mock.patch('time.time', mock.Mock(return_value=1234567)) |
345 | + def test_notify_error(self): |
346 | + # It is possible to notify that an error occurred during a deployment. |
347 | + deployment_id = self.observer.add_deployment() |
348 | + watcher = self.observer.deployments[deployment_id] |
349 | + self.observer.notify_completed(deployment_id, error='bad wolf') |
350 | + expected = { |
351 | + 'DeploymentId': deployment_id, |
352 | + 'Status': utils.COMPLETED, |
353 | + 'Time': 1234567, |
354 | + 'Error': 'bad wolf', |
355 | + } |
356 | + self.assertEqual(expected, watcher.getlast()) |
357 | + self.assertTrue(watcher.closed) |
358 | + |
359 | + |
360 | class TestRequireAuthenticatedUser( |
361 | helpers.BundlesTestMixin, LogTrapTestCase, AsyncTestCase): |
362 | |
363 | |
364 | === modified file 'server/guiserver/tests/test_utils.py' |
365 | --- server/guiserver/tests/test_utils.py 2013-07-25 14:22:50 +0000 |
366 | +++ server/guiserver/tests/test_utils.py 2013-08-21 16:12:55 +0000 |
367 | @@ -17,14 +17,63 @@ |
368 | """Tests for the Juju GUI server utilities.""" |
369 | |
370 | import json |
371 | +import os |
372 | +import shutil |
373 | +import tempfile |
374 | import unittest |
375 | |
376 | import mock |
377 | -from tornado.testing import ExpectLog |
378 | +from tornado import ( |
379 | + concurrent, |
380 | + gen, |
381 | +) |
382 | +from tornado.testing import ( |
383 | + AsyncTestCase, |
384 | + ExpectLog, |
385 | + gen_test, |
386 | +) |
387 | |
388 | from guiserver import utils |
389 | |
390 | |
391 | +class TestAddFuture(AsyncTestCase): |
392 | + |
393 | + def setUp(self): |
394 | + # Set up a future object and a result attribute where tests will store |
395 | + # their results. |
396 | + super(TestAddFuture, self).setUp() |
397 | + self.future = concurrent.Future() |
398 | + self.result = None |
399 | + |
400 | + @gen.coroutine |
401 | + def assert_done(self, result): |
402 | + """Fire the future and ensure the callback has been called. |
403 | + |
404 | + Callbacks in this test case store their results in self.result. |
405 | + """ |
406 | + self.assertTrue(self.future.done()) |
407 | + yield self.future |
408 | + self.assertEqual(result, self.result) |
409 | + |
410 | + @gen_test |
411 | + def test_without_args(self): |
412 | + # A callback without args is correctly called. |
413 | + def callback(future): |
414 | + self.result = 'future said: ' + future.result() |
415 | + utils.add_future(self.io_loop, self.future, callback) |
416 | + self.future.set_result('I am done') |
417 | + yield self.assert_done('future said: I am done') |
418 | + |
419 | + @gen_test |
420 | + def test_with_args(self): |
421 | + # A callback with args is correctly called. |
422 | + def callback(arg1, arg2, future): |
423 | + self.result = [arg1, arg2, future.result()] |
424 | + utils.add_future(self.io_loop, self.future, callback, 1, 2) |
425 | + self.future.set_result(3) |
426 | + yield self.assert_done([1, 2, 3]) |
427 | + |
428 | + |
429 | class TestGetHeaders(unittest.TestCase): |
430 | |
431 | def test_propagation(self): |
432 | @@ -64,6 +113,54 @@ |
433 | self.assertIsNone(utils.json_decode_dict('"not-a-dict"')) |
434 | |
435 | |
436 | +class TestMkdir(unittest.TestCase): |
437 | + |
438 | + def setUp(self): |
439 | + self.playground = tempfile.mkdtemp() |
440 | + self.addCleanup(shutil.rmtree, self.playground) |
441 | + |
442 | + def test_create_dir(self): |
443 | + # A directory is correctly created. |
444 | + path = os.path.join(self.playground, 'foo') |
445 | + utils.mkdir(path) |
446 | + self.assertTrue(os.path.isdir(path)) |
447 | + |
448 | + def test_intermediate_dirs(self): |
449 | + # All intermediate directories are created. |
450 | + path = os.path.join(self.playground, 'foo', 'bar', 'leaf') |
451 | + utils.mkdir(path) |
452 | + self.assertTrue(os.path.isdir(path)) |
453 | + |
454 | + def test_expand_user(self): |
455 | + # The ~ construction is expanded. |
456 | + with mock.patch('os.environ', {'HOME': self.playground}): |
457 | + utils.mkdir('~/in/my/home') |
458 | + path = os.path.join(self.playground, 'in', 'my', 'home') |
459 | + self.assertTrue(os.path.isdir(path)) |
460 | + |
461 | + def test_existing_dir(self): |
462 | + # The function exits without errors if the target directory exists. |
463 | + path = os.path.join(self.playground, 'foo') |
464 | + os.mkdir(path) |
465 | + utils.mkdir(path) |
466 | + |
467 | + def test_existing_file(self): |
468 | + # An OSError is raised if a file already exists in the target path. |
469 | + path = os.path.join(self.playground, 'foo') |
470 | + with open(path, 'w'): |
471 | + with self.assertRaises(OSError): |
472 | + utils.mkdir(path) |
473 | + |
474 | + def test_failure(self): |
475 | + # Errors are correctly re-raised. |
476 | + path = os.path.join(self.playground, 'foo') |
477 | + os.chmod(self.playground, 0000) |
478 | + self.addCleanup(os.chmod, self.playground, 0700) |
479 | + with self.assertRaises(OSError): |
480 | + utils.mkdir(os.path.join(path)) |
481 | + self.assertFalse(os.path.exists(path)) |
482 | + |
483 | + |
484 | class TestRequestSummary(unittest.TestCase): |
485 | |
486 | def test_summary(self): |
487 | |
488 | === modified file 'server/guiserver/utils.py' |
489 | --- server/guiserver/utils.py 2013-07-24 12:35:50 +0000 |
490 | +++ server/guiserver/utils.py 2013-08-21 16:12:55 +0000 |
491 | @@ -17,12 +17,24 @@ |
492 | """Juju GUI server utility functions and classes.""" |
493 | |
494 | import collections |
495 | +import errno |
496 | +import functools |
497 | import logging |
498 | +import os |
499 | import urlparse |
500 | |
501 | from tornado import escape |
502 | |
503 | |
504 | +def add_future(io_loop, future, callback, *args): |
505 | + """Schedule a callback on the IO loop when the given Future is finished. |
506 | + |
507 | + The callback will receive the given optional args and the completed Future. |
508 | + """ |
509 | + partial_callback = functools.partial(callback, *args) |
510 | + io_loop.add_future(future, partial_callback) |
511 | + |
512 | + |
513 | def get_headers(request, websocket_url): |
514 | """Return additional headers to be included in the client connection. |
515 | |
516 | @@ -55,6 +67,21 @@ |
517 | return data |
518 | |
519 | |
520 | +def mkdir(path): |
521 | + """Create a leaf directory and all intermediate ones. |
522 | + |
523 | + Also expand ~ and ~user constructions. |
524 | + If path exists and it's a directory, return without errors. |
525 | + """ |
526 | + path = os.path.expanduser(path) |
527 | + try: |
528 | + os.makedirs(path) |
529 | + except OSError as err: |
530 | + # Re-raise the error if the target path exists but it is not a dir. |
531 | + if (err.errno != errno.EEXIST) or (not os.path.isdir(path)): |
532 | + raise |
533 | + |
534 | + |
535 | def request_summary(request): |
536 | """Return a string representing a summary for the given request.""" |
537 | return '{} {} ({})'.format(request.method, request.uri, request.remote_ip) |
Reviewers: mp+181269_ code.launchpad. net,
Message:
Please take a look.
Description:
GUI server: base deployer helpers.
This branch includes some helper functions
and objects that are required to implement
the Deployer and DeployMiddleware classes.
To avoid this branch to include too many
changes, the base classes above will be
implemented in a separate branch.
Tests: `make unittest` from the branch root.
https:/ /code.launchpad .net/~frankban/ charms/ precise/ juju-gui/ guiserver- bundles- base-helpers/ +merge/ 181269
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/13153043/
Affected files: guiserver/ bundles/ __init_ _.py guiserver/ bundles/ utils.py guiserver/ tests/bundles/ test_utils. py guiserver/ tests/test_ utils.py guiserver/ utils.py
A [revision details]
M revision
M server/
M server/
M server/
M server/
M server/