Merge lp:~frankban/charms/precise/juju-gui/guiserver-bundles-integration into lp:~juju-gui/charms/precise/juju-gui/trunk

Proposed by Francesco Banconi
Status: Merged
Merged at revision: 96
Proposed branch: lp:~frankban/charms/precise/juju-gui/guiserver-bundles-integration
Merge into: lp:~juju-gui/charms/precise/juju-gui/trunk
Diff against target: 625 lines (+250/-36)
14 files modified
hooks/backend.py (+3/-2)
hooks/utils.py (+12/-8)
revision (+1/-1)
server/guiserver/__init__.py (+1/-1)
server/guiserver/apps.py (+13/-1)
server/guiserver/handlers.py (+37/-6)
server/guiserver/tests/helpers.py (+13/-0)
server/guiserver/tests/test_apps.py (+33/-6)
server/guiserver/tests/test_handlers.py (+66/-3)
server/guiserver/tests/test_utils.py (+40/-0)
server/guiserver/utils.py (+24/-0)
server/setup.py (+2/-0)
tests/test_backends.py (+1/-2)
tests/test_utils.py (+4/-6)
To merge this branch: bzr merge lp:~frankban/charms/precise/juju-gui/guiserver-bundles-integration
Reviewer Review Type Date Requested Status
charmers Pending
Review via email: mp+182067@code.launchpad.net

Description of the change

GUI server: bundles support integration.

This branch enables the GUI server bundles support.
Changed the hooks so that the new GUI server
dependencies are installed.
Also added more info to the info handler.

Tests: `make unittest` from the branch root.

QA:

In the steps below I assume your default juju is
juju-core and your juju-core env is named "go".

- Bootstrap a juju-core environment:

  juju bootstrap -e go --upload-tools

- From the root of this branch, deploy and expose
  the GUI running the following:

  make deploy JUJU_ENV=go

  The command above will exit when the GUI is ready.

- Switch to the GUI server, then wait a minute for the
  server to be ready:

  juju set -e go juju-gui builtin-server=true

- In a separate terminal tab, start watching the GUI server log
  (the first line should be "starting Juju GUI server v0.1.0"):

  juju ssh -e go 1 sudo tailf /var/log/upstart/guiserver.log

- Use the browser to navigate the GUI, log in and check
  everything works fine.

- Also visit the /gui-server-info URL path: you should see
  something like:
  {
    "uptime": 77,
    "deployer": [],
    "apiversion": "go",
    "version": "0.1.0",
    "debug": false,
    "apiurl": "wss://ec2-54-211-156-178.compute-1.amazonaws.com:17070"
  }

- Let's live test the deployer support: to do that, we will
  use the script in http://pastebin.ubuntu.com/6028073/

- Download the script, edit the PASSWORD value (line 17) and
  execute it passing the Juju GUI node address as first argument, e.g.:

    python start-deployer.py ec2-54-227-188-14.compute-1.amazonaws.com

- The script runs several API calls, simulates errors
  in the API parameters (which should also be notified in the GUI
  server logs), starts two deployments and start watching the
  second one. It takes some minutes to complete. In the meanwhile,
  you should be able to see the relevant changes in the topology view.
  At the end of the process, the GUI should show three started
  services (wordpress and mysql, connected to each other, and
  mediawiki), and the output of the script should be similar to
  this one: http://pastebin.ubuntu.com/6028121/

  Note that I added a card to create a charm functional test that
  automates this live check.

- Switch back to the legacy server (haproxy + apaxche2):

  juju set -e go juju-gui builtin-server=false

- Refresh the browser and check everything is ok.

Done, thank you!

https://codereview.appspot.com/12741051/

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

Reviewers: mp+182067_code.launchpad.net,

Message:
Please take a look.

Description:
GUI server: bundles support integration.

This branch enables the GUI server bundles support.
Changed the hooks so that the new GUI server
dependencies are installed.
Also added more info to the info handler.

Tests: `make unittest` from the branch root.

QA:

In the steps below I assume your default juju is
juju-core and your juju-core env is named "go".

- Bootstrap a juju-core environment:

   juju bootstrap -e go --upload-tools

- From the root of this branch, deploy and expose
   the GUI running the following:

   make deploy JUJU_ENV=go

   The command above will exit when the GUI is ready.

- Switch to the GUI server, then wait a minute for the
   server to be ready:

   juju set -e go juju-gui builtin-server=true

- In a separate terminal tab, start watching the GUI server log
   (the first line should be "starting Juju GUI server v0.1.0"):

   juju ssh -e go 1 sudo tailf /var/log/upstart/guiserver.log

- Use the browser to navigate the GUI, log in and check
   everything works fine.

- Also visit the /gui-server-info URL path: you should see
   something like:
   {
     "uptime": 77,
     "deployer": [],
     "apiversion": "go",
     "version": "0.1.0",
     "debug": false,
     "apiurl": "wss://ec2-54-211-156-178.compute-1.amazonaws.com:17070"
   }

- Let's live test the deployer support: to do that, we will
   use the script in http://pastebin.ubuntu.com/6028073/

- Download the script, edit the PASSWORD value (line 17) and
   execute it passing the Juju GUI node address as first argument, e.g.:

     python start-deployer.py ec2-54-227-188-14.compute-1.amazonaws.com

- The script runs several API calls, simulates errors
   in the API parameters (which should also be notified in the GUI
   server logs), starts two deployments and start watching the
   second one. It takes some minutes to complete. In the meanwhile,
   you should be able to see the relevant changes in the topology view.
   At the end of the process, the GUI should show three started
   services (wordpress and mysql, connected to each other, and
   mediawiki), and the output of the script should be similar to
   this one: http://pastebin.ubuntu.com/6028121/

   Note that I added a card to create a charm functional test that
   automates this live check.

- Switch back to the legacy server (haproxy + apaxche2):

   juju set -e go juju-gui builtin-server=false

- Refresh the browser and check everything is ok.

Done, thank you!

https://code.launchpad.net/~frankban/charms/precise/juju-gui/guiserver-bundles-integration/+merge/182067

(do not edit description out of merge proposal)

Please review this at https://codereview.appspot.com/12741051/

Affected files:
   A [revision details]
   A deps/futures-2.1.4.tar.gz
   A deps/juju-deployer-0.2.2.tar.gz
   A deps/jujuclient-0.0.9.tar.gz
   M hooks/backend.py
   M hooks/utils.py
   M revision
   M server/guiserver/__init__.py
   M server/guiserver/apps.py
   M server/guiserver/handlers.py
   M server/guiserver/tests/helpers.py
   M server/guiserver/tests/test_apps.py
   M server/guiserver/tests/test_handlers.py
   M server/guiserver/tests/...

Read more...

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

Code LGTM and the QA worked perfectly.

Thanks for the detailed QA instructions.

https://codereview.appspot.com/12741051/diff/1/server/guiserver/utils.py
File server/guiserver/utils.py (right):

https://codereview.appspot.com/12741051/diff/1/server/guiserver/utils.py#newcode101
server/guiserver/utils.py:101: handler_ref = weakref.ref(handler)
Interesting!

https://codereview.appspot.com/12741051/

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

*** Submitted:

GUI server: bundles support integration.

This branch enables the GUI server bundles support.
Changed the hooks so that the new GUI server
dependencies are installed.
Also added more info to the info handler.

Tests: `make unittest` from the branch root.

QA:

In the steps below I assume your default juju is
juju-core and your juju-core env is named "go".

- Bootstrap a juju-core environment:

   juju bootstrap -e go --upload-tools

- From the root of this branch, deploy and expose
   the GUI running the following:

   make deploy JUJU_ENV=go

   The command above will exit when the GUI is ready.

- Switch to the GUI server, then wait a minute for the
   server to be ready:

   juju set -e go juju-gui builtin-server=true

- In a separate terminal tab, start watching the GUI server log
   (the first line should be "starting Juju GUI server v0.1.0"):

   juju ssh -e go 1 sudo tailf /var/log/upstart/guiserver.log

- Use the browser to navigate the GUI, log in and check
   everything works fine.

- Also visit the /gui-server-info URL path: you should see
   something like:
   {
     "uptime": 77,
     "deployer": [],
     "apiversion": "go",
     "version": "0.1.0",
     "debug": false,
     "apiurl": "wss://ec2-54-211-156-178.compute-1.amazonaws.com:17070"
   }

- Let's live test the deployer support: to do that, we will
   use the script in http://pastebin.ubuntu.com/6028073/

- Download the script, edit the PASSWORD value (line 17) and
   execute it passing the Juju GUI node address as first argument, e.g.:

     python start-deployer.py ec2-54-227-188-14.compute-1.amazonaws.com

- The script runs several API calls, simulates errors
   in the API parameters (which should also be notified in the GUI
   server logs), starts two deployments and start watching the
   second one. It takes some minutes to complete. In the meanwhile,
   you should be able to see the relevant changes in the topology view.
   At the end of the process, the GUI should show three started
   services (wordpress and mysql, connected to each other, and
   mediawiki), and the output of the script should be similar to
   this one: http://pastebin.ubuntu.com/6028121/

   Note that I added a card to create a charm functional test that
   automates this live check.

- Switch back to the legacy server (haproxy + apaxche2):

   juju set -e go juju-gui builtin-server=false

- Refresh the browser and check everything is ok.

Done, thank you!

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

https://codereview.appspot.com/12741051/

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

Thanks Brad and Rick for the reviews!

https://codereview.appspot.com/12741051/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'deps/futures-2.1.4.tar.gz'
2Binary files deps/futures-2.1.4.tar.gz 1970-01-01 00:00:00 +0000 and deps/futures-2.1.4.tar.gz 2013-08-26 10:30:37 +0000 differ
3=== added file 'deps/juju-deployer-0.2.2.tar.gz'
4Binary files deps/juju-deployer-0.2.2.tar.gz 1970-01-01 00:00:00 +0000 and deps/juju-deployer-0.2.2.tar.gz 2013-08-26 10:30:37 +0000 differ
5=== added file 'deps/jujuclient-0.0.9.tar.gz'
6Binary files deps/jujuclient-0.0.9.tar.gz 1970-01-01 00:00:00 +0000 and deps/jujuclient-0.0.9.tar.gz 2013-08-26 10:30:37 +0000 differ
7=== modified file 'hooks/backend.py'
8--- hooks/backend.py 2013-08-08 13:53:22 +0000
9+++ hooks/backend.py 2013-08-26 10:30:37 +0000
10@@ -183,10 +183,11 @@
11 class BuiltinServerMixin(ServerInstallMixinBase):
12 """Manage the builtin server via Upstart."""
13
14- debs = ('openssl', 'python-pip')
15+ # The package python-bzrlib is required by juju-deployer.
16+ # The package python-pip is is used to install the GUI server dependencies.
17+ debs = ('openssl', 'python-bzrlib', 'python-pip')
18
19 def install(self, backend):
20- utils.install_tornado()
21 utils.install_builtin_server()
22 self._setup_certificates(backend)
23
24
25=== modified file 'hooks/utils.py'
26--- hooks/utils.py 2013-08-09 13:41:19 +0000
27+++ hooks/utils.py 2013-08-26 10:30:37 +0000
28@@ -107,8 +107,14 @@
29 CONFIG_DIR = os.path.join(CURRENT_DIR, 'config')
30 JUJU_DIR = os.path.join(CURRENT_DIR, 'juju')
31 JUJU_GUI_DIR = os.path.join(CURRENT_DIR, 'juju-gui')
32+# Builtin server dependencies. The order of these requirements is important.
33+SERVER_DEPENDENCIES = (
34+ 'futures-2.1.4.tar.gz',
35+ 'tornado-3.1.tar.gz',
36+ 'jujuclient-0.0.9.tar.gz',
37+ 'juju-deployer-0.2.2.tar.gz',
38+)
39 SERVER_DIR = os.path.join(CURRENT_DIR, 'server')
40-TORNADO_PATH = os.path.join(CURRENT_DIR, 'deps', 'tornado-3.1.tar.gz')
41
42 APACHE_CFG_DIR = os.path.join(os.path.sep, 'etc', 'apache2')
43 APACHE_PORTS = os.path.join(APACHE_CFG_DIR, 'ports.conf')
44@@ -525,15 +531,13 @@
45 remove_apache_setup()
46
47
48-def install_tornado():
49- """Install Tornado from a local tarball."""
50- log('Installing Tornado.')
51- with su('root'):
52- cmd_log(run('pip', 'install', TORNADO_PATH))
53-
54-
55 def install_builtin_server():
56 """Install the builtin server code."""
57+ log('Installing the builtin server dependencies.')
58+ for dependency_name in SERVER_DEPENDENCIES:
59+ dependency = os.path.join(CURRENT_DIR, 'deps', dependency_name)
60+ with su('root'):
61+ cmd_log(run('pip', 'install', dependency))
62 log('Installing the builtin server.')
63 setup_cmd = os.path.join(SERVER_DIR, 'setup.py')
64 with su('root'):
65
66=== modified file 'revision'
67--- revision 2013-08-21 12:02:49 +0000
68+++ revision 2013-08-26 10:30:37 +0000
69@@ -1,1 +1,1 @@
70-74
71+75
72
73=== modified file 'server/guiserver/__init__.py'
74--- server/guiserver/__init__.py 2013-07-19 10:25:55 +0000
75+++ server/guiserver/__init__.py 2013-08-26 10:30:37 +0000
76@@ -30,7 +30,7 @@
77 the HTTPS connection, allowing changes in the Juju environment to be propagated
78 and shown immediately by the browser. """
79
80-VERSION = (0, 0, 1)
81+VERSION = (0, 1, 0)
82
83
84 def get_version():
85
86=== modified file 'server/guiserver/apps.py'
87--- server/guiserver/apps.py 2013-08-19 11:50:15 +0000
88+++ server/guiserver/apps.py 2013-08-26 10:30:37 +0000
89@@ -17,6 +17,7 @@
90 """Juju GUI server applications."""
91
92 import os
93+import time
94
95 from tornado import web
96 from tornado.options import options
97@@ -25,6 +26,7 @@
98 auth,
99 handlers,
100 )
101+from guiserver.bundles.base import Deployer
102
103
104 def server():
105@@ -36,12 +38,22 @@
106 # Set up static paths.
107 guiroot = options.guiroot
108 static_path = os.path.join(guiroot, 'juju-ui')
109+ # Set up the bundle deployer.
110+ deployer = Deployer(options.apiurl, options.apiversion)
111 # Set up handlers.
112 websocket_handler_options = {
113 # The Juju API backend url.
114 'apiurl': options.apiurl,
115 # The backend to use for user authentication.
116 'auth_backend': auth.get_backend(options.apiversion),
117+ # The Juju deployer to use for importing bundles.
118+ 'deployer': deployer,
119+ }
120+ info_handler_options = {
121+ 'apiurl': options.apiurl,
122+ 'apiversion': options.apiversion,
123+ 'deployer': deployer,
124+ 'start_time': int(time.time()),
125 }
126 server_handlers = [
127 # Handle WebSocket connections.
128@@ -50,7 +62,7 @@
129 (r'^/juju-ui/(.*)', web.StaticFileHandler, {'path': static_path}),
130 (r'^/(favicon\.ico)$', web.StaticFileHandler, {'path': guiroot}),
131 # Handle GUI server info.
132- (r'^/gui-server-info', handlers.InfoHandler),
133+ (r'^/gui-server-info', handlers.InfoHandler, info_handler_options),
134 ]
135 if options.testsroot:
136 params = {'path': options.testsroot, 'default_filename': 'index.html'}
137
138=== modified file 'server/guiserver/handlers.py'
139--- server/guiserver/handlers.py 2013-08-19 12:03:10 +0000
140+++ server/guiserver/handlers.py 2013-08-26 10:30:37 +0000
141@@ -19,6 +19,7 @@
142 from collections import deque
143 import logging
144 import os
145+import time
146
147 from tornado import (
148 escape,
149@@ -33,11 +34,13 @@
150 AuthMiddleware,
151 User,
152 )
153+from guiserver.bundles.base import DeployMiddleware
154 from guiserver.clients import websocket_connect
155 from guiserver.utils import (
156 get_headers,
157 json_decode_dict,
158 request_summary,
159+ wrap_write_message,
160 )
161
162
163@@ -45,7 +48,8 @@
164 """WebSocket handler supporting secure WebSockets.
165
166 This handler acts as a proxy between the browser connection and the
167- Juju API server.
168+ Juju API server. It also handles API authentication and requests for
169+ bundles deployment (using the juju-deployer deployment format).
170
171 Relevant attributes:
172
173@@ -66,7 +70,7 @@
174 """
175
176 @gen.coroutine
177- def initialize(self, apiurl, auth_backend, io_loop=None):
178+ def initialize(self, apiurl, auth_backend, deployer, io_loop=None):
179 """Initialize the WebSocket server.
180
181 Create a new WebSocket client and connect it to the Juju API.
182@@ -84,6 +88,9 @@
183 # Set up the authentication infrastructure.
184 self.user = User()
185 self.auth = AuthMiddleware(self.user, auth_backend)
186+ # Set up the bundle deployment infrastructure.
187+ self.deployment = DeployMiddleware(
188+ self.user, deployer, wrap_write_message(self))
189 # Juju requires the Origin header to be included in the WebSocket
190 # client handshake request. Propagate the client origin if present;
191 # use the Juju API server as origin otherwise.
192@@ -111,13 +118,20 @@
193 def on_message(self, message):
194 """Hook called when a new message is received from the browser.
195
196- The message is propagated to the Juju API server.
197+ If the message is a deployment request, start the deployment process.
198+ Otherwise the message is propagated to the Juju API server.
199 Messages sent before the client connection to the Juju API server is
200 established are queued for later delivery.
201 """
202 data = json_decode_dict(message)
203- if (data is not None) and (not self.user.is_authenticated):
204- self.auth.process_request(data)
205+ if data is not None:
206+ # Handle deployment requests.
207+ if self.deployment.requested(data):
208+ return self.deployment.process_request(data)
209+ # Handle authentication requests.
210+ if not self.user.is_authenticated:
211+ self.auth.process_request(data)
212+ # Propagate messages to the Juju API server.
213 if self.juju_connected:
214 logging.debug(self._summary + 'client -> juju: {}'.format(message))
215 return self.juju_connection.write_message(message)
216@@ -176,9 +190,26 @@
217 class InfoHandler(web.RequestHandler):
218 """Return information about the GUI server."""
219
220+ def initialize(self, apiurl, apiversion, deployer, start_time):
221+ """Initialize the handler."""
222+ self.apiurl = apiurl
223+ self.apiversion = apiversion
224+ self.deployer = deployer
225+ self.start_time = start_time
226+
227+ def get_info(self, settings):
228+ return {
229+ 'apiurl': self.apiurl,
230+ 'apiversion': self.apiversion,
231+ 'debug': settings.get('debug', False),
232+ 'deployer': self.deployer.status(),
233+ 'uptime': int(time.time()) - self.start_time,
234+ 'version': get_version(),
235+ }
236+
237 def get(self):
238 """Handle GET requests."""
239- info = {'version': get_version()}
240+ info = self.get_info(self.application.settings)
241 self.write(escape.json_encode(info))
242
243
244
245=== modified file 'server/guiserver/tests/helpers.py'
246--- server/guiserver/tests/helpers.py 2013-08-23 16:06:21 +0000
247+++ server/guiserver/tests/helpers.py 2013-08-26 10:30:37 +0000
248@@ -227,6 +227,19 @@
249 }
250 return json.dumps(data) if encoded else data
251
252+ def make_deployment_response(
253+ self, request_id=42, response=None, error=None, encoded=False):
254+ """Create and return a deployment response message.
255+
256+ If encoded is set to True, the returned message will be JSON encoded.
257+ """
258+ if response is None:
259+ response = {}
260+ data = {'RequestId': request_id, 'Response': response}
261+ if error is not None:
262+ data['Error'] = error
263+ return json.dumps(data) if encoded else data
264+
265 def patch_validate(self, side_effect=None):
266 """Mock the blocking validate function."""
267 mock_validate = MultiProcessMock(side_effect=side_effect)
268
269=== modified file 'server/guiserver/tests/test_apps.py'
270--- server/guiserver/tests/test_apps.py 2013-08-09 13:26:54 +0000
271+++ server/guiserver/tests/test_apps.py 2013-08-26 10:30:37 +0000
272@@ -22,8 +22,11 @@
273
274 from guiserver import (
275 apps,
276+ auth,
277 handlers,
278+ manage,
279 )
280+from guiserver.bundles import base
281
282
283 class AppsTestMixin(object):
284@@ -43,6 +46,18 @@
285 return spec
286 return None
287
288+ def assert_in_spec(self, spec, key, value=None):
289+ """Ensure the given key-value pair is present in the specification.
290+
291+ Also return the value in the specification.
292+ """
293+ self.assertIsNotNone(spec)
294+ self.assertIn(key, spec.kwargs)
295+ obtained = spec.kwargs[key]
296+ if value is not None:
297+ self.assertEqual(value, obtained)
298+ return obtained
299+
300 def test_debug_enabled(self):
301 # Debug mode is enabled if options.debug is True.
302 app = self.get_app(debug=True)
303@@ -67,21 +82,33 @@
304 with mock.patch('guiserver.apps.options', options):
305 return apps.server()
306
307+ def test_auth_backend(self):
308+ # The authentication backend instance is correctly passed to the
309+ # WebSocket handler.
310+ app = self.get_app()
311+ spec = self.get_url_spec(app, r'^/ws$')
312+ auth_backend = self.assert_in_spec(spec, 'auth_backend')
313+ expected = auth.get_backend(manage.DEFAULT_API_VERSION)
314+ self.assertIsInstance(auth_backend, type(expected))
315+
316+ def test_deployer(self):
317+ # The deployer instance is correctly passed to the WebSocket handler.
318+ app = self.get_app()
319+ spec = self.get_url_spec(app, r'^/ws$')
320+ deployer = self.assert_in_spec(spec, 'deployer')
321+ self.assertIsInstance(deployer, base.Deployer)
322+
323 def test_static_files(self):
324 # The Juju GUI static files are correctly served.
325 app = self.get_app()
326 spec = self.get_url_spec(app, r'^/juju-ui/(.*)$')
327- self.assertIsNotNone(spec)
328- self.assertIn('path', spec.kwargs)
329- self.assertEqual('/my/guiroot/juju-ui', spec.kwargs['path'])
330+ self.assert_in_spec(spec, 'path', value='/my/guiroot/juju-ui')
331
332 def test_serving_gui_tests(self):
333 # The server can be configured to serve GUI unit tests.
334 app = self.get_app(testsroot='/my/tests/')
335 spec = self.get_url_spec(app, r'^/test/(.*)$')
336- self.assertIsNotNone(spec)
337- self.assertIn('path', spec.kwargs)
338- self.assertEqual('/my/tests/', spec.kwargs['path'])
339+ self.assert_in_spec(spec, 'path', value='/my/tests/')
340
341 def test_not_serving_gui_tests(self):
342 # The server can be configured to avoid serving GUI unit tests.
343
344=== modified file 'server/guiserver/tests/test_handlers.py'
345--- server/guiserver/tests/test_handlers.py 2013-08-19 12:22:33 +0000
346+++ server/guiserver/tests/test_handlers.py 2013-08-26 10:30:37 +0000
347@@ -43,6 +43,7 @@
348 handlers,
349 manage,
350 )
351+from guiserver.bundles import base
352 from guiserver.tests import helpers
353
354
355@@ -63,6 +64,8 @@
356 # ws-echo-server -> ws-forwarding-client -> ws-server -> ws-client
357 self.apiurl = self.get_wss_url('/echo')
358 self.api_close_future = concurrent.Future()
359+ self.deployer = base.Deployer(
360+ self.apiurl, manage.DEFAULT_API_VERSION, io_loop=self.io_loop)
361 echo_options = {
362 'close_future': self.api_close_future,
363 'io_loop': self.io_loop,
364@@ -70,6 +73,7 @@
365 ws_options = {
366 'apiurl': self.apiurl,
367 'auth_backend': self.auth_backend,
368+ 'deployer': self.deployer,
369 'io_loop': self.io_loop,
370 }
371 return web.Application([
372@@ -103,7 +107,8 @@
373 apiurl = self.apiurl
374 handler = self.make_handler(
375 headers=headers, mock_protocol=mock_protocol)
376- yield handler.initialize(apiurl, self.auth_backend, self.io_loop)
377+ yield handler.initialize(
378+ apiurl, self.auth_backend, self.deployer, self.io_loop)
379 raise gen.Return(handler)
380
381
382@@ -304,6 +309,48 @@
383 self.assertFalse(self.handler.auth.in_progress())
384
385
386+class TestWebSocketHandlerBundles(
387+ WebSocketHandlerTestMixin, helpers.WSSTestMixin,
388+ helpers.BundlesTestMixin, LogTrapTestCase, AsyncHTTPSTestCase):
389+
390+ @gen_test
391+ def test_bundle_import_process(self):
392+ # The bundle import process is correctly started and completed.
393+ write_message_path = 'guiserver.handlers.wrap_write_message'
394+ with mock.patch(write_message_path) as mock_write_message:
395+ handler = yield self.make_initialized_handler()
396+ # Simulate the user is authenticated.
397+ handler.user.is_authenticated = True
398+ # Start a bundle import.
399+ request = self.make_deployment_request('Import', encoded=True)
400+ with self.patch_validate(), self.patch_import_bundle():
401+ yield handler.on_message(request)
402+ expected = self.make_deployment_response(response={'DeploymentId': 0})
403+ mock_write_message().assert_called_once_with(expected)
404+ # Start observing the deployment progress.
405+ request = self.make_deployment_request('Watch', encoded=True)
406+ yield handler.on_message(request)
407+ expected = self.make_deployment_response(response={'WatcherId': 0})
408+ mock_write_message().assert_called_with(expected)
409+ # Get the two next changes: in the first one the deployment has been
410+ # started, in the second one it is completed. This way the test runner
411+ # can safely stop the IO loop (no remaining Future callbacks).
412+ request = self.make_deployment_request('Next', encoded=True)
413+ yield handler.on_message(request)
414+ yield handler.on_message(request)
415+
416+ @gen_test
417+ def test_not_authenticated(self):
418+ # The bundle deployment support is only activated for logged in users.
419+ client = yield self.make_client()
420+ request = self.make_deployment_request('Import', encoded=True)
421+ client.write_message(request)
422+ expected = self.make_deployment_response(
423+ error='unauthorized access: no user logged in')
424+ response = yield client.read_message()
425+ self.assertEqual(expected, json.loads(response))
426+
427+
428 class TestIndexHandler(LogTrapTestCase, AsyncHTTPTestCase):
429
430 def setUp(self):
431@@ -343,11 +390,27 @@
432 class TestInfoHandler(LogTrapTestCase, AsyncHTTPTestCase):
433
434 def get_app(self):
435- return web.Application([(r'^/info', handlers.InfoHandler)])
436+ mock_deployer = mock.Mock()
437+ mock_deployer.status.return_value = 'deployments status'
438+ options = {
439+ 'apiurl': 'wss://api.example.com:17070',
440+ 'apiversion': 'clojure',
441+ 'deployer': mock_deployer,
442+ 'start_time': 10,
443+ }
444+ return web.Application([(r'^/info', handlers.InfoHandler, options)])
445
446+ @mock.patch('time.time', mock.Mock(return_value=52))
447 def test_info(self):
448 # The handler correctly returns information about the GUI server.
449- expected = {'version': get_version()}
450+ expected = {
451+ 'apiurl': 'wss://api.example.com:17070',
452+ 'apiversion': 'clojure',
453+ 'debug': False,
454+ 'deployer': 'deployments status',
455+ 'uptime': 42,
456+ 'version': get_version(),
457+ }
458 response = self.fetch('/info')
459 self.assertEqual(200, response.code)
460 info = escape.json_decode(response.body)
461
462=== modified file 'server/guiserver/tests/test_utils.py'
463--- server/guiserver/tests/test_utils.py 2013-08-21 11:39:01 +0000
464+++ server/guiserver/tests/test_utils.py 2013-08-26 10:30:37 +0000
465@@ -170,6 +170,46 @@
466 self.assertEqual('GET /path (127.0.0.1)', summary)
467
468
469+class TestWrapWriteMessage(unittest.TestCase):
470+
471+ expected_log = "discarding message \(closed connection\): 'hello'"
472+
473+ def setUp(self):
474+ self.messages = []
475+ self.handler = type(
476+ 'Handler', (),
477+ {'connected': True, 'write_message': self.messages.append}
478+ )()
479+ self.wrapped = utils.wrap_write_message(self.handler)
480+
481+ def test_propagated(self):
482+ # The JSON encoded version of the message is correctly propagated.
483+ self.wrapped({'foo': 'bar'})
484+ self.assertEqual(json.dumps({'foo': 'bar'}), self.messages[0])
485+
486+ def test_multiple_messages(self):
487+ # Multiple messages are correctly propagated.
488+ self.wrapped(1)
489+ self.wrapped(2)
490+ self.wrapped(3)
491+ self.assertEqual(['1', '2', '3'], self.messages)
492+
493+ def test_connection_closed(self):
494+ # If the handler connection is closed, a warning is logged and the
495+ # wrapped method is not called.
496+ self.handler.connected = False
497+ with ExpectLog('', self.expected_log, required=True):
498+ self.wrapped('hello')
499+ self.assertEqual([], self.messages)
500+
501+ def test_handler_deleted(self):
502+ # A warning is logged if the referred handler has been deleted.
503+ del self.handler
504+ with ExpectLog('', self.expected_log, required=True):
505+ self.wrapped('hello')
506+ self.assertEqual([], self.messages)
507+
508+
509 class TestWsToHttp(unittest.TestCase):
510
511 def test_websocket(self):
512
513=== modified file 'server/guiserver/utils.py'
514--- server/guiserver/utils.py 2013-08-21 11:39:01 +0000
515+++ server/guiserver/utils.py 2013-08-26 10:30:37 +0000
516@@ -22,6 +22,7 @@
517 import logging
518 import os
519 import urlparse
520+import weakref
521
522 from tornado import escape
523
524@@ -87,6 +88,29 @@
525 return '{} {} ({})'.format(request.method, request.uri, request.remote_ip)
526
527
528+def wrap_write_message(handler):
529+ """Wrap the write_message() method of the given handler.
530+
531+ The resulting function uses a weak reference to the handler, in order to
532+ avoid calling the wrapped method if the handler (a WebSocket connection)
533+ has been closed or garbage collected.
534+
535+ If the handler is still there, and the connection is still established,
536+ JSON encode the received data before propagating it.
537+ """
538+ handler_ref = weakref.ref(handler)
539+
540+ def wrapped(data):
541+ handler = handler_ref()
542+ if (handler is None) or (not handler.connected):
543+ return logging.warning(
544+ 'discarding message (closed connection): {!r}'.format(data))
545+ message = escape.json_encode(data)
546+ handler.write_message(message)
547+
548+ return wrapped
549+
550+
551 def ws_to_http(url):
552 """Return the HTTP(S) equivalent of the provided ws/wss URL."""
553 parts = urlparse.urlsplit(url)
554
555=== modified file 'server/setup.py'
556--- server/setup.py 2013-08-07 15:08:16 +0000
557+++ server/setup.py 2013-08-26 10:30:37 +0000
558@@ -38,7 +38,9 @@
559 keywords='juju gui server',
560 packages=[
561 PROJECT_NAME,
562+ '{}.bundles'.format(PROJECT_NAME),
563 '{}.tests'.format(PROJECT_NAME),
564+ '{}.tests.bundles'.format(PROJECT_NAME),
565 ],
566 scripts=['runserver.py'],
567 classifiers=[
568
569=== modified file 'tests/test_backends.py'
570--- tests/test_backends.py 2013-08-07 10:29:06 +0000
571+++ tests/test_backends.py 2013-08-26 10:30:37 +0000
572@@ -119,7 +119,6 @@
573 'get_api_address': utils.get_api_address,
574 'get_npm_cache_archive_url': utils.get_npm_cache_archive_url,
575 'install_builtin_server': utils.install_builtin_server,
576- 'install_tornado': utils.install_tornado,
577 'parse_source': utils.parse_source,
578 'prime_npm_cache': utils.prime_npm_cache,
579 'remove_apache_setup': utils.remove_apache_setup,
580@@ -200,7 +199,7 @@
581 test_backend.install()
582 for mocked in (
583 'apt_get_install', 'fetch_api', 'find_missing_packages',
584- 'install_builtin_server', 'install_tornado',
585+ 'install_builtin_server',
586 ):
587 self.assertTrue(
588 self.called.get(mocked), '{} was not called'.format(mocked))
589
590=== modified file 'tests/test_utils.py'
591--- tests/test_utils.py 2013-08-09 13:30:49 +0000
592+++ tests/test_utils.py 2013-08-26 10:30:37 +0000
593@@ -33,6 +33,7 @@
594 API_PORT,
595 JUJU_GUI_DIR,
596 JUJU_PEM,
597+ SERVER_DEPENDENCIES,
598 WEB_PORT,
599 _get_by_attr,
600 cmd_log,
601@@ -46,7 +47,6 @@
602 parse_source,
603 get_npm_cache_archive_url,
604 install_builtin_server,
605- install_tornado,
606 remove_apache_setup,
607 remove_haproxy_setup,
608 render_to_file,
609@@ -722,13 +722,11 @@
610 self.assertEqual(self.service_names, ['haproxy', 'apache2'])
611 self.assertEqual(self.actions, [charmhelpers.STOP, charmhelpers.STOP])
612
613- def test_install_tornado(self):
614- install_tornado()
615- self.assertEqual(self.run_call_count, 1)
616-
617 def test_install_builtin_server(self):
618 install_builtin_server()
619- self.assertEqual(self.run_call_count, 1)
620+ # The function executes one "pip install" call for each dependency, and
621+ # a final "python setup.py" call for the GUI server itself.
622+ self.assertEqual(self.run_call_count, len(SERVER_DEPENDENCIES) + 1)
623
624 def test_write_builtin_server_startup(self):
625 write_builtin_server_startup(

Subscribers

People subscribed via source and target branches