Merge lp:~frankban/charms/precise/juju-gui/server-base into lp:~juju-gui/charms/precise/juju-gui/trunk
- Precise Pangolin (12.04)
- server-base
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 76 |
Proposed branch: | lp:~frankban/charms/precise/juju-gui/server-base |
Merge into: | lp:~juju-gui/charms/precise/juju-gui/trunk |
Diff against target: |
1134 lines (+990/-4) 21 files modified
.bzrignore (+3/-0) Dependencies.md (+1/-1) Makefile (+3/-1) Operation.md (+29/-0) README.md (+2/-1) revision (+1/-1) server/guiserver/__init__.py (+38/-0) server/guiserver/apps.py (+54/-0) server/guiserver/clients.py (+104/-0) server/guiserver/handlers.py (+87/-0) server/guiserver/manage.py (+81/-0) server/guiserver/tests/__init__.py (+21/-0) server/guiserver/tests/helpers.py (+66/-0) server/guiserver/tests/test_clients.py (+86/-0) server/guiserver/tests/test_handlers.py (+184/-0) server/guiserver/tests/test_manage.py (+91/-0) server/runserver.py (+29/-0) server/runtests.py (+36/-0) server/setup.py (+53/-0) tests/11-server.test (+19/-0) tests/requirements.pip (+2/-0) |
To merge this branch: | bzr merge lp:~frankban/charms/precise/juju-gui/server-base |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
charmers | Pending | ||
Review via email: mp+175624@code.launchpad.net |
Commit message
Description of the change
Initial implementation of the Juju GUI server.
This first cut of the server replaces the
functionality provided in the charm by
haproxy and apache.
The server includes:
- static files serving;
- a WebSocket proxy handling the
connections between the browser and
the server and between the server and
the Juju API;
- HTTP to HTTPS redirecting.
Further features like auth and deployer
management will be added in the future.
ws4py is used to implement the WebSocket
client: there is something to fix there and
it will be done in a future branch.
At this time there is no easy way to QA the
server, but tests can be run using
`make unittest`: the test/lint suite is already
integrated with the one used by the charm.
Unfortunately the diff is quite long, sorry!
Francesco Banconi (frankban) wrote : | # |
Gary Poster (gary) wrote : | # |
Wow, code LGTM. I'll try to run tests now...
https:/
File server/
https:/
server/
can be properly set up.
Mm, so tornado has import side effects? :-/ too bad.
Brad Crittenden (bac) wrote : | # |
LGTM: I echo Gary's 'wow' -- good work. I'll coordinate with Gary and
see if I need to run the tests too.
https:/
File Operation.md (right):
https:/
Operation.md:22: 443. HTTP connections to the 80 port are redirected to
the former one.
change to: connections to port 80 are
https:/
File server/
https:/
server/
Both browser-server and server- Juju
Super trivial:
Remove the space: server-Juju
https:/
File server/
https:/
server/
I don't understand how this only redirects HTTP - HTTPS. Why doesn't
HTTPS traffic go to this handler?
https:/
File server/
https:/
server/
I'd say something about the optional *args and **kwargs. Not sure what.
https:/
server/
self.sock here.
typo: redefining
https:/
server/
self.sock here.
same typo
https:/
server/
ws4py.
Maybe explain what you're referring to here.
https:/
File server/
https:/
server/
Oh, there it is! NM.
https:/
File server/
https:/
server/
Why does this test not use self.received like the next one does?
- 94. By Francesco Banconi
-
Changes as per review.
- 95. By Francesco Banconi
-
Bumped revision up.
Francesco Banconi (frankban) wrote : | # |
Please take a look.
https:/
File Operation.md (right):
https:/
Operation.md:22: 443. HTTP connections to the 80 port are redirected to
the former one.
On 2013/07/18 21:52:56, bac wrote:
> change to: connections to port 80 are
Done.
https:/
File server/
https:/
server/
Both browser-server and server- Juju
On 2013/07/18 21:52:56, bac wrote:
> Super trivial:
> Remove the space: server-Juju
Done.
https:/
File server/
https:/
server/
can be properly set up.
On 2013/07/18 21:07:31, gary.poster wrote:
> Mm, so tornado has import side effects? :-/ too bad.
No, actually I will replace this with a regulat module level import.
I misinterpreted the doc: "All modules that define options must have
been imported before the command line is parsed."
We do that in manage.setup(), so I guess this can be changed.
https:/
File server/
https:/
server/
On 2013/07/18 21:52:56, bac wrote:
> I'd say something about the optional *args and **kwargs. Not sure
what.
Done.
https:/
server/
self.sock here.
On 2013/07/18 21:52:56, bac wrote:
> typo: redefining
Done.
https:/
server/
self.sock here.
On 2013/07/18 21:52:56, bac wrote:
> same typo
Done.
https:/
server/
ws4py.
On 2013/07/18 21:52:56, bac wrote:
> Maybe explain what you're referring to here.
Done.
https:/
File server/
https:/
server/
On 2013/07/18 21:52:56, bac wrote:
> Why does this test not use self.received like the next one does?
Here we want to just check that a message can be correctly received by
the client.
In the next test we also check that, when a message is received, a
callback (i.e. self.received.
Francesco Banconi (frankban) wrote : | # |
Thank you both for the reviews!
Francesco Banconi (frankban) wrote : | # |
*** Submitted:
Initial implementation of the Juju GUI server.
This first cut of the server replaces the
functionality provided in the charm by
haproxy and apache.
The server includes:
- static files serving;
- a WebSocket proxy handling the
connections between the browser and
the server and between the server and
the Juju API;
- HTTP to HTTPS redirecting.
Further features like auth and deployer
management will be added in the future.
ws4py is used to implement the WebSocket
client: there is something to fix there and
it will be done in a future branch.
At this time there is no easy way to QA the
server, but tests can be run using
`make unittest`: the test/lint suite is already
integrated with the one used by the charm.
Unfortunately the diff is quite long, sorry!
R=gary.poster, bac
CC=
https:/
Kapil Thangavelu (hazmat) wrote : | # |
Just poking through the charm and came across this, its really nice
work, well done.
> R=gary.poster, bac
> CC=
> https:/
Preview Diff
1 | === modified file '.bzrignore' |
2 | --- .bzrignore 2013-06-06 15:39:13 +0000 |
3 | +++ .bzrignore 2013-07-19 10:28:27 +0000 |
4 | @@ -2,4 +2,7 @@ |
5 | tags |
6 | exec.d/* |
7 | npm-cache.tgz |
8 | +server/build |
9 | +server/dist |
10 | +server/MANIFEST |
11 | tests/.venv |
12 | |
13 | === modified file 'Dependencies.md' |
14 | --- Dependencies.md 2013-06-11 14:04:04 +0000 |
15 | +++ Dependencies.md 2013-07-19 10:28:27 +0000 |
16 | @@ -47,7 +47,7 @@ |
17 | Organizations deploying the charm for their enterprise may have the |
18 | requirement to not allow the installation of software from outside of their |
19 | local network. Typically those environments require all external software to |
20 | -be downloaded to a local server and used from there. Our devel PPA provides a |
21 | +be downloaded to a local server and used from there. Our stable PPA provides a |
22 | single starting place to obtain QA'd software. Dev ops can grab the subset of |
23 | packages they need, audit, test, and then serve them locally. The |
24 | `repository-location` config variable can be used to point to the local repo. |
25 | |
26 | === modified file 'Makefile' |
27 | --- Makefile 2013-06-12 09:07:29 +0000 |
28 | +++ Makefile 2013-07-19 10:28:27 +0000 |
29 | @@ -24,6 +24,7 @@ |
30 | |
31 | unittest: setup |
32 | ./tests/10-unit.test |
33 | + ./tests/11-server.test |
34 | |
35 | ftest: setup |
36 | $(JUJUTEST) 20-functional.test |
37 | @@ -36,7 +37,8 @@ |
38 | $(JUJUTEST) |
39 | |
40 | lint: setup |
41 | - @$(VENV)/bin/flake8 --show-source --exclude=.venv ./hooks/ ./tests/ |
42 | + @$(VENV)/bin/flake8 --show-source --exclude=.venv \ |
43 | + ./hooks/ ./tests/ ./server/ |
44 | |
45 | clean: |
46 | find . -name '*.pyc' -delete |
47 | |
48 | === added file 'Operation.md' |
49 | --- Operation.md 1970-01-01 00:00:00 +0000 |
50 | +++ Operation.md 2013-07-19 10:28:27 +0000 |
51 | @@ -0,0 +1,29 @@ |
52 | +<!-- |
53 | +Operation.md |
54 | +Copyright 2013 Canonical Ltd. |
55 | +This work is licensed under the Creative Commons Attribution-Share Alike 3.0 |
56 | +Unported License. To view a copy of this license, visit |
57 | +http://creativecommons.org/licenses/by-sa/3.0/ or send a letter to Creative |
58 | +Commons, 171 Second Street, Suite 300, San Francisco, California, 94105, USA. |
59 | +--> |
60 | + |
61 | +# Juju GUI Charm Operation # |
62 | + |
63 | +## How it works ## |
64 | + |
65 | +The Juju GUI is a client-side, JavaScript application that runs inside a |
66 | +web browser. The browser connects to a custom-made server deployed by |
67 | +the charm. |
68 | + |
69 | +## Server ## |
70 | + |
71 | +The server directly serves static files to the browser, including |
72 | +images, HTML, CSS and JavaScript files via an HTTPS connection to port |
73 | +443. HTTP connections to port 80 are redirected to the former one. |
74 | +All other URLs serve the common `index.html` file. |
75 | + |
76 | +It also acts as a proxy between the browser and the Juju API server that |
77 | +performs the actual orchestration work. Both browser-server and server- |
78 | +Juju connections are bidirectional, using the WebSocket protocol on the |
79 | +same port as the HTTPS connection, allowing changes in the Juju |
80 | +environment to be propagated and shown immediately by the browser. |
81 | |
82 | === modified file 'README.md' |
83 | --- README.md 2013-07-17 21:30:28 +0000 |
84 | +++ README.md 2013-07-19 10:28:27 +0000 |
85 | @@ -13,7 +13,8 @@ |
86 | |
87 | ## Supported Browsers ## |
88 | |
89 | -The Juju GUI supports recent releases of Chrome, Chromium and Firefox. |
90 | +The Juju GUI supports recent releases of the Chrome, Chromium and Firefox web |
91 | +browsers. |
92 | |
93 | ## Demo/Staging Server ## |
94 | |
95 | |
96 | === modified file 'revision' |
97 | --- revision 2013-07-18 19:02:19 +0000 |
98 | +++ revision 2013-07-19 10:28:27 +0000 |
99 | @@ -1,1 +1,1 @@ |
100 | -57 |
101 | +58 |
102 | |
103 | === added directory 'server' |
104 | === added directory 'server/guiserver' |
105 | === added file 'server/guiserver/__init__.py' |
106 | --- server/guiserver/__init__.py 1970-01-01 00:00:00 +0000 |
107 | +++ server/guiserver/__init__.py 2013-07-19 10:28:27 +0000 |
108 | @@ -0,0 +1,38 @@ |
109 | +# This file is part of the Juju GUI, which lets users view and manage Juju |
110 | +# environments within a graphical interface (https://launchpad.net/juju-gui). |
111 | +# Copyright (C) 2013 Canonical Ltd. |
112 | +# |
113 | +# This program is free software: you can redistribute it and/or modify it under |
114 | +# the terms of the GNU Affero General Public License version 3, as published by |
115 | +# the Free Software Foundation. |
116 | +# |
117 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
118 | +# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, |
119 | +# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
120 | +# Affero General Public License for more details. |
121 | +# |
122 | +# You should have received a copy of the GNU Affero General Public License |
123 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
124 | + |
125 | +"""Juju GUI server. |
126 | + |
127 | +The GUI server is a custom-made application based on the |
128 | +[Tornado](http://www.tornadoweb.org/) framework. |
129 | + |
130 | +It directly serves static files to the browser, including images, HTML, CSS and |
131 | +JavaScript files via an HTTPS connection to port 443. HTTP connections to port |
132 | +80 are redirected to the former one. All other URLs serve the common |
133 | +`index.html` file. |
134 | + |
135 | +It also acts as a proxy between the browser and the Juju API server that |
136 | +performs the actual orchestration work. Both browser-server and server-Juju |
137 | +connections are bidirectional, using the WebSocket protocol on the same port as |
138 | +the HTTPS connection, allowing changes in the Juju environment to be propagated |
139 | +and shown immediately by the browser. """ |
140 | + |
141 | +VERSION = (0, 0, 1) |
142 | + |
143 | + |
144 | +def get_version(): |
145 | + """Return the Juju GUI server version as a string.""" |
146 | + return '.'.join(map(str, VERSION)) |
147 | |
148 | === added file 'server/guiserver/apps.py' |
149 | --- server/guiserver/apps.py 1970-01-01 00:00:00 +0000 |
150 | +++ server/guiserver/apps.py 2013-07-19 10:28:27 +0000 |
151 | @@ -0,0 +1,54 @@ |
152 | +# This file is part of the Juju GUI, which lets users view and manage Juju |
153 | +# environments within a graphical interface (https://launchpad.net/juju-gui). |
154 | +# Copyright (C) 2013 Canonical Ltd. |
155 | +# |
156 | +# This program is free software: you can redistribute it and/or modify it under |
157 | +# the terms of the GNU Affero General Public License version 3, as published by |
158 | +# the Free Software Foundation. |
159 | +# |
160 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
161 | +# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, |
162 | +# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
163 | +# Affero General Public License for more details. |
164 | +# |
165 | +# You should have received a copy of the GNU Affero General Public License |
166 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
167 | + |
168 | +"""Juju GUI server applications.""" |
169 | + |
170 | +import os |
171 | + |
172 | +from tornado import web |
173 | +from tornado.options import options |
174 | + |
175 | +from guiserver import handlers |
176 | + |
177 | + |
178 | +def server(): |
179 | + """Return the main server application. |
180 | + |
181 | + The server app is responsible for serving the WebSocket connection, the |
182 | + Juju GUI static files and the main index file for dynamic URLs. |
183 | + """ |
184 | + guiroot = options.guiroot |
185 | + static_path = os.path.join(guiroot, 'juju-ui') |
186 | + return web.Application([ |
187 | + # Handle WebSocket connections. |
188 | + (r'^/ws$', handlers.WebSocketHandler, {'jujuapi': options.jujuapi}), |
189 | + # Handle static files. |
190 | + (r'^/juju-ui/(.*)', web.StaticFileHandler, {'path': static_path}), |
191 | + (r'^/(favicon\.ico)$', web.StaticFileHandler, {'path': guiroot}), |
192 | + # Any other path is served by index.html. |
193 | + (r'^/(.*)', handlers.IndexHandler, {'path': guiroot}), |
194 | + ], debug=options.debug) |
195 | + |
196 | + |
197 | +def redirector(): |
198 | + """Return the redirector application. |
199 | + |
200 | + The redirector app is responsible for redirecting HTTP traffic to HTTPS. |
201 | + """ |
202 | + return web.Application([ |
203 | + # Redirect all HTTP traffic to HTTPS. |
204 | + (r'.*', handlers.HttpsRedirectHandler), |
205 | + ], debug=options.debug) |
206 | |
207 | === added file 'server/guiserver/clients.py' |
208 | --- server/guiserver/clients.py 1970-01-01 00:00:00 +0000 |
209 | +++ server/guiserver/clients.py 2013-07-19 10:28:27 +0000 |
210 | @@ -0,0 +1,104 @@ |
211 | +# This file is part of the Juju GUI, which lets users view and manage Juju |
212 | +# environments within a graphical interface (https://launchpad.net/juju-gui). |
213 | +# Copyright (C) 2013 Canonical Ltd. |
214 | +# |
215 | +# This program is free software: you can redistribute it and/or modify it under |
216 | +# the terms of the GNU Affero General Public License version 3, as published by |
217 | +# the Free Software Foundation. |
218 | +# |
219 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
220 | +# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, |
221 | +# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
222 | +# Affero General Public License for more details. |
223 | +# |
224 | +# You should have received a copy of the GNU Affero General Public License |
225 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
226 | + |
227 | +"""Juju GUI server websocket clients.""" |
228 | + |
229 | +from collections import deque |
230 | +import logging |
231 | + |
232 | +from tornado.concurrent import Future |
233 | +from ws4py.client import tornadoclient |
234 | + |
235 | + |
236 | +class WebSocketClient(tornadoclient.TornadoWebSocketClient): |
237 | + """WebSocket client implementation supporting secure WebSockets.""" |
238 | + |
239 | + def __init__(self, url, on_message_received, *args, **kwargs): |
240 | + """Client initializer. |
241 | + |
242 | + The WebSocket client receives two arguments: |
243 | + - url: the WebSocket URL to use for the connection; |
244 | + - on_message_received: a callback that will be called each time a |
245 | + new message is received by the client. |
246 | + |
247 | + It also accepts all the args and kwargs accepted by |
248 | + ws4py.client.tornadoclient.TornadoWebSocketClient. |
249 | + """ |
250 | + super(WebSocketClient, self).__init__(url, *args, **kwargs) |
251 | + self.connected = False |
252 | + self._connected_future = Future() |
253 | + self._closed_future = Future() |
254 | + self._queue = deque() |
255 | + self._on_message_received = on_message_received |
256 | + |
257 | + def connect(self, *args, **kwargs): |
258 | + super(WebSocketClient, self).connect(*args, **kwargs) |
259 | + return self._connected_future |
260 | + |
261 | + def opened(self): |
262 | + """Hook called when the connection is initially established.""" |
263 | + logging.debug('ws client: connected') |
264 | + self._connected_future.set_result(None) |
265 | + self.connected = True |
266 | + # Send all the messages that have been enqueued before the connection |
267 | + # was established. |
268 | + queue = self._queue |
269 | + while self.connected and len(queue): |
270 | + self.send(queue.popleft()) |
271 | + |
272 | + def send(self, message, *args, **kwargs): |
273 | + """Override to fix the socket problem.""" |
274 | + # FIXME: find a way to avoid redefining self.sock here. |
275 | + self.sock = self.io.socket |
276 | + logging.debug('ws client: send message: {}'.format(message)) |
277 | + super(WebSocketClient, self).send(message, *args, **kwargs) |
278 | + |
279 | + def write_message(self, message): |
280 | + """Send a message on the WebSocket connection. |
281 | + |
282 | + Wrap self.send so that messages sent before the connection is |
283 | + established are queued for later delivery. |
284 | + """ |
285 | + if self.connected: |
286 | + logging.debug('ws client: send message: {}'.format(message)) |
287 | + return self.send(message) |
288 | + logging.debug('ws client: queue message: {}'.format(message)) |
289 | + self._queue.append(message) |
290 | + |
291 | + def received_message(self, message): |
292 | + """Hook called when a new message is received.""" |
293 | + logging.debug('ws client: received message: {}'.format(message)) |
294 | + self._on_message_received(message.data) |
295 | + |
296 | + def close(self, *args, **kwargs): |
297 | + # FIXME: find a way to avoid redefining self.sock here. |
298 | + self.sock = self.io.socket |
299 | + super(WebSocketClient, self).close(*args, **kwargs) |
300 | + return self._closed_future |
301 | + |
302 | + def closed(self, code, reason=None): |
303 | + """Hook called when the connection is terminated.""" |
304 | + logging.debug('ws client: closed ({})'.format(code)) |
305 | + # FIXME: closed should be called only once. |
306 | + if not self._closed_future.done(): |
307 | + self._closed_future.set_result(None) |
308 | + self.connected = False |
309 | + |
310 | + def _cleanup(self, *args, **kwargs): |
311 | + # FIXME: this seems clearly an error in ws4py. The internal |
312 | + # TornadoWebSocketClient.__stream_closed method calls an undefined |
313 | + # self._cleanup(). |
314 | + pass |
315 | |
316 | === added file 'server/guiserver/handlers.py' |
317 | --- server/guiserver/handlers.py 1970-01-01 00:00:00 +0000 |
318 | +++ server/guiserver/handlers.py 2013-07-19 10:28:27 +0000 |
319 | @@ -0,0 +1,87 @@ |
320 | +# This file is part of the Juju GUI, which lets users view and manage Juju |
321 | +# environments within a graphical interface (https://launchpad.net/juju-gui). |
322 | +# Copyright (C) 2013 Canonical Ltd. |
323 | +# |
324 | +# This program is free software: you can redistribute it and/or modify it under |
325 | +# the terms of the GNU Affero General Public License version 3, as published by |
326 | +# the Free Software Foundation. |
327 | +# |
328 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
329 | +# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, |
330 | +# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
331 | +# Affero General Public License for more details. |
332 | +# |
333 | +# You should have received a copy of the GNU Affero General Public License |
334 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
335 | + |
336 | +"""Juju GUI server HTTP/HTTPS handlers.""" |
337 | + |
338 | +import logging |
339 | +import os |
340 | + |
341 | +from tornado import ( |
342 | + gen, |
343 | + web, |
344 | + websocket, |
345 | +) |
346 | + |
347 | +from guiserver.clients import WebSocketClient |
348 | + |
349 | + |
350 | +class WebSocketHandler(websocket.WebSocketHandler): |
351 | + """WebSocket handler supporting secure WebSockets. |
352 | + |
353 | + This handler acts as a proxy between the browser connection and the |
354 | + Juju API server. |
355 | + """ |
356 | + |
357 | + @gen.coroutine |
358 | + def initialize(self, jujuapi): |
359 | + """Create a new WebSocket client and connect it to the Juju API.""" |
360 | + logging.debug('ws server: connecting to juju') |
361 | + self.jujuconn = WebSocketClient(jujuapi, self.on_juju_message) |
362 | + yield self.jujuconn.connect() |
363 | + logging.debug('ws server: connected to juju') |
364 | + |
365 | + def on_message(self, message): |
366 | + """Hook called when a new message is received from the browser. |
367 | + |
368 | + The message is propagated to the Juju API server. |
369 | + """ |
370 | + logging.debug('ws server: browser --> juju: {}'.format(message)) |
371 | + self.jujuconn.write_message(message) |
372 | + |
373 | + def on_juju_message(self, message): |
374 | + """Hook called when a new message is received from the Juju API server. |
375 | + |
376 | + The message is propagated to the browser. |
377 | + """ |
378 | + logging.debug('ws server: juju --> browser: {}'.format(message)) |
379 | + self.write_message(message) |
380 | + |
381 | + @gen.coroutine |
382 | + def on_close(self): |
383 | + """Hook called when the WebSocket connection is terminated.""" |
384 | + logging.debug('ws server: connection closed') |
385 | + yield self.jujuconn.close() |
386 | + self.jujuconn = None |
387 | + |
388 | + |
389 | +class IndexHandler(web.StaticFileHandler): |
390 | + """Serve all requests using the index.html file placed in the static root. |
391 | + """ |
392 | + |
393 | + @classmethod |
394 | + def get_absolute_path(cls, root, path): |
395 | + """See tornado.web.StaticFileHandler.get_absolute_path.""" |
396 | + return os.path.join(root, 'index.html') |
397 | + |
398 | + |
399 | +class HttpsRedirectHandler(web.RequestHandler): |
400 | + """Permanently redirect all the requests to the equivalent HTTPS URL.""" |
401 | + |
402 | + def get(self): |
403 | + """Handle GET requests.""" |
404 | + request = self.request |
405 | + url = 'https://{}{}'.format(request.host, request.uri) |
406 | + self.redirect(url, permanent=True) |
407 | |
408 | === added file 'server/guiserver/manage.py' |
409 | --- server/guiserver/manage.py 1970-01-01 00:00:00 +0000 |
410 | +++ server/guiserver/manage.py 2013-07-19 10:28:27 +0000 |
411 | @@ -0,0 +1,81 @@ |
412 | +# This file is part of the Juju GUI, which lets users view and manage Juju |
413 | +# environments within a graphical interface (https://launchpad.net/juju-gui). |
414 | +# Copyright (C) 2013 Canonical Ltd. |
415 | +# |
416 | +# This program is free software: you can redistribute it and/or modify it under |
417 | +# the terms of the GNU Affero General Public License version 3, as published by |
418 | +# the Free Software Foundation. |
419 | +# |
420 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
421 | +# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, |
422 | +# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
423 | +# Affero General Public License for more details. |
424 | +# |
425 | +# You should have received a copy of the GNU Affero General Public License |
426 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
427 | + |
428 | +"""Juju GUI server management.""" |
429 | + |
430 | +import logging |
431 | +import sys |
432 | + |
433 | +from tornado.ioloop import IOLoop |
434 | +from tornado.options import ( |
435 | + define, |
436 | + options, |
437 | + parse_command_line, |
438 | +) |
439 | + |
440 | +import guiserver |
441 | +from guiserver.apps import ( |
442 | + redirector, |
443 | + server, |
444 | +) |
445 | + |
446 | + |
447 | +SSL_OPTIONS = { |
448 | + 'certfile': '/etc/ssl/juju-gui/juju.crt', |
449 | + 'keyfile': '/etc/ssl/juju-gui/juju.key', |
450 | +} |
451 | + |
452 | + |
453 | +def _add_debug(logger): |
454 | + """Add a debug option to the option parser. |
455 | + |
456 | + The debug option is True if --logging=DEBUG is passed, False otherwise. |
457 | + """ |
458 | + debug = logger.level == logging.DEBUG |
459 | + options.define('debug', default=debug) |
460 | + |
461 | + |
462 | +def _validate_required(*args): |
463 | + """Validate required arguments. |
464 | + |
465 | + Exit with an error if a mandatory argument is missing. |
466 | + """ |
467 | + for name in args: |
468 | + try: |
469 | + value = options[name].strip() |
470 | + except AttributeError: |
471 | + value = '' |
472 | + if not value: |
473 | + sys.exit('error: the {} argument is required'.format(name)) |
474 | + |
475 | + |
476 | +def setup(): |
477 | + """Set up options and logger.""" |
478 | + define('guiroot', type=str, help='the Juju GUI static files path') |
479 | + define('jujuapi', type=str, help='the Juju WebSocket server address') |
480 | + # In Tornado, parsing the options also sets up the default logger. |
481 | + parse_command_line() |
482 | + _validate_required('guiroot', 'jujuapi') |
483 | + _add_debug(logging.getLogger()) |
484 | + |
485 | + |
486 | +def run(): |
487 | + """Run the server""" |
488 | + server().listen(443, ssl_options=SSL_OPTIONS) |
489 | + redirector().listen(80) |
490 | + version = guiserver.get_version() |
491 | + logging.info('starting Juju GUI server v{}'.format(version)) |
492 | + IOLoop.instance().start() |
493 | |
494 | === added directory 'server/guiserver/tests' |
495 | === added file 'server/guiserver/tests/__init__.py' |
496 | --- server/guiserver/tests/__init__.py 1970-01-01 00:00:00 +0000 |
497 | +++ server/guiserver/tests/__init__.py 2013-07-19 10:28:27 +0000 |
498 | @@ -0,0 +1,21 @@ |
499 | +# This file is part of the Juju GUI, which lets users view and manage Juju |
500 | +# environments within a graphical interface (https://launchpad.net/juju-gui). |
501 | +# Copyright (C) 2013 Canonical Ltd. |
502 | +# |
503 | +# This program is free software: you can redistribute it and/or modify it under |
504 | +# the terms of the GNU Affero General Public License version 3, as published by |
505 | +# the Free Software Foundation. |
506 | +# |
507 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
508 | +# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, |
509 | +# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
510 | +# Affero General Public License for more details. |
511 | +# |
512 | +# You should have received a copy of the GNU Affero General Public License |
513 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
514 | + |
515 | +"""Juju GUI server test suite. |
516 | + |
517 | +This suite uses the Tornado unit testing helpers for asynchronous code and the |
518 | +Tornado test runner: see <http://www.tornadoweb.org/en/stable/testing.html>. |
519 | +""" |
520 | |
521 | === added file 'server/guiserver/tests/helpers.py' |
522 | --- server/guiserver/tests/helpers.py 1970-01-01 00:00:00 +0000 |
523 | +++ server/guiserver/tests/helpers.py 2013-07-19 10:28:27 +0000 |
524 | @@ -0,0 +1,66 @@ |
525 | +# This file is part of the Juju GUI, which lets users view and manage Juju |
526 | +# environments within a graphical interface (https://launchpad.net/juju-gui). |
527 | +# Copyright (C) 2013 Canonical Ltd. |
528 | +# |
529 | +# This program is free software: you can redistribute it and/or modify it under |
530 | +# the terms of the GNU Affero General Public License version 3, as published by |
531 | +# the Free Software Foundation. |
532 | +# |
533 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
534 | +# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, |
535 | +# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
536 | +# Affero General Public License for more details. |
537 | +# |
538 | +# You should have received a copy of the GNU Affero General Public License |
539 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
540 | + |
541 | +"""Juju GUI server test utilities.""" |
542 | + |
543 | +from tornado import ( |
544 | + concurrent, |
545 | + websocket, |
546 | +) |
547 | + |
548 | +from guiserver import clients |
549 | + |
550 | + |
551 | +class EchoWebSocketHandler(websocket.WebSocketHandler): |
552 | + """A WebSocket server echoing back messages.""" |
553 | + |
554 | + def initialize(self, close_future): |
555 | + """The given future will be fired when the connection is terminated.""" |
556 | + self._closed_future = close_future |
557 | + |
558 | + def on_message(self, message): |
559 | + self.write_message(message, isinstance(message, bytes)) |
560 | + |
561 | + def on_close(self): |
562 | + self._closed_future.set_result(None) |
563 | + |
564 | + |
565 | +class WebSocketClient(clients.WebSocketClient): |
566 | + """Used to test the guiserver.clients.WebSocketClient.""" |
567 | + |
568 | + _message_received_future = None |
569 | + |
570 | + def send(self, *args, **kwargs): |
571 | + super(WebSocketClient, self).send(*args, **kwargs) |
572 | + self._message_received_future = concurrent.Future() |
573 | + return self._message_received_future |
574 | + |
575 | + def write_message(self, *args, **kwargs): |
576 | + super(WebSocketClient, self).write_message(*args, **kwargs) |
577 | + self._message_received_future = concurrent.Future() |
578 | + return self._message_received_future |
579 | + |
580 | + def received_message(self, message, *args, **kwargs): |
581 | + super(WebSocketClient, self).received_message(message, *args, **kwargs) |
582 | + self._message_received_future.set_result(message.data) |
583 | + |
584 | + |
585 | +class WSSTestMixin(object): |
586 | + """Add some helper methods for testing secure WebSocket handlers.""" |
587 | + |
588 | + def get_wss_url(self, path): |
589 | + """Return an absolute secure WebSocket url for the given path.""" |
590 | + return 'wss://localhost:{}{}'.format(self.get_http_port(), path) |
591 | |
592 | === added file 'server/guiserver/tests/test_clients.py' |
593 | --- server/guiserver/tests/test_clients.py 1970-01-01 00:00:00 +0000 |
594 | +++ server/guiserver/tests/test_clients.py 2013-07-19 10:28:27 +0000 |
595 | @@ -0,0 +1,86 @@ |
596 | +# This file is part of the Juju GUI, which lets users view and manage Juju |
597 | +# environments within a graphical interface (https://launchpad.net/juju-gui). |
598 | +# Copyright (C) 2013 Canonical Ltd. |
599 | +# |
600 | +# This program is free software: you can redistribute it and/or modify it under |
601 | +# the terms of the GNU Affero General Public License version 3, as published by |
602 | +# the Free Software Foundation. |
603 | +# |
604 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
605 | +# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, |
606 | +# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
607 | +# Affero General Public License for more details. |
608 | +# |
609 | +# You should have received a copy of the GNU Affero General Public License |
610 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
611 | + |
612 | +"""Tests for the Juju GUI server clients.""" |
613 | + |
614 | +from tornado import ( |
615 | + concurrent, |
616 | + web, |
617 | +) |
618 | +from tornado.testing import ( |
619 | + AsyncHTTPSTestCase, |
620 | + gen_test, |
621 | +) |
622 | + |
623 | +from guiserver.tests import helpers |
624 | + |
625 | + |
626 | +class TestWebSocketClient(AsyncHTTPSTestCase, helpers.WSSTestMixin): |
627 | + |
628 | + def setUp(self): |
629 | + self.received = [] |
630 | + self.server_closed_future = concurrent.Future() |
631 | + super(TestWebSocketClient, self).setUp() |
632 | + # Now that the app is set up, we can create the WebSocket client. |
633 | + self.client = helpers.WebSocketClient( |
634 | + self.get_wss_url('/'), self.received.append, io_loop=self.io_loop) |
635 | + |
636 | + def get_app(self): |
637 | + # In this test case the WebSocket client is connected to a WebSocket |
638 | + # echo server returning received messages. |
639 | + options = {'close_future': self.server_closed_future} |
640 | + return web.Application([(r'/', helpers.EchoWebSocketHandler, options)]) |
641 | + |
642 | + @gen_test |
643 | + def test_initial_connection(self): |
644 | + # The client correctly establishes a connection to the server. |
645 | + yield self.client.connect() |
646 | + self.assertTrue(self.client.connected) |
647 | + |
648 | + @gen_test |
649 | + def test_send_receive(self): |
650 | + # The client correctly sends and receives messages on the secure |
651 | + # WebSocket connection. |
652 | + yield self.client.connect() |
653 | + message = yield self.client.write_message('hello') |
654 | + self.assertEqual('hello', message) |
655 | + |
656 | + @gen_test |
657 | + def test_callback(self): |
658 | + # The client executes the given callback each time a message is |
659 | + # received. |
660 | + yield self.client.connect() |
661 | + yield self.client.write_message('hello') |
662 | + yield self.client.write_message('world') |
663 | + self.assertEqual(['hello', 'world'], self.received) |
664 | + |
665 | + @gen_test |
666 | + def test_queued_messages(self): |
667 | + # Messages sent before the connection is established are preserved and |
668 | + # sent right after the connection is opened. |
669 | + self.client.write_message('hello') |
670 | + yield self.client.connect() |
671 | + yield self.client.write_message('world') |
672 | + yield self.client.close() |
673 | + self.assertEqual(['hello', 'world'], self.received) |
674 | + |
675 | + @gen_test |
676 | + def test_connection_close(self): |
677 | + # The client connection is correctly terminated. |
678 | + yield self.client.connect() |
679 | + yield self.client.close() |
680 | + self.assertFalse(self.client.connected) |
681 | + yield self.server_closed_future |
682 | |
683 | === added file 'server/guiserver/tests/test_handlers.py' |
684 | --- server/guiserver/tests/test_handlers.py 1970-01-01 00:00:00 +0000 |
685 | +++ server/guiserver/tests/test_handlers.py 2013-07-19 10:28:27 +0000 |
686 | @@ -0,0 +1,184 @@ |
687 | +# This file is part of the Juju GUI, which lets users view and manage Juju |
688 | +# environments within a graphical interface (https://launchpad.net/juju-gui). |
689 | +# Copyright (C) 2013 Canonical Ltd. |
690 | +# |
691 | +# This program is free software: you can redistribute it and/or modify it under |
692 | +# the terms of the GNU Affero General Public License version 3, as published by |
693 | +# the Free Software Foundation. |
694 | +# |
695 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
696 | +# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, |
697 | +# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
698 | +# Affero General Public License for more details. |
699 | +# |
700 | +# You should have received a copy of the GNU Affero General Public License |
701 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
702 | + |
703 | +"""Tests for the Juju GUI server handlers.""" |
704 | + |
705 | +import os |
706 | +import shutil |
707 | +import tempfile |
708 | + |
709 | +import mock |
710 | +from tornado import ( |
711 | + concurrent, |
712 | + web, |
713 | +) |
714 | +from tornado.testing import ( |
715 | + AsyncHTTPTestCase, |
716 | + AsyncHTTPSTestCase, |
717 | + gen_test, |
718 | + LogTrapTestCase, |
719 | +) |
720 | + |
721 | +from guiserver import ( |
722 | + clients, |
723 | + handlers, |
724 | +) |
725 | +from guiserver.tests import helpers |
726 | + |
727 | + |
728 | +class TestWebSocketHandler(AsyncHTTPSTestCase, helpers.WSSTestMixin): |
729 | + |
730 | + def get_app(self): |
731 | + # In this test case a WebSocket server is created. The server creates a |
732 | + # new client on each request. This client should forward messages to a |
733 | + # WebSocket echo server. In order to test the communication, some of |
734 | + # the tests create another client that connects to the server, e.g.: |
735 | + # ws-client -> ws-server -> ws-forwarding-client -> ws-echo-server |
736 | + # Messages arriving to the echo server are returned back to the client: |
737 | + # ws-echo-server -> ws-forwarding-client -> ws-server -> ws-client |
738 | + self.echo_server_address = self.get_wss_url('/echo') |
739 | + self.echo_server_closed_future = concurrent.Future() |
740 | + echo_options = {'close_future': self.echo_server_closed_future} |
741 | + ws_options = {'jujuapi': self.echo_server_address} |
742 | + return web.Application([ |
743 | + (r'/echo', helpers.EchoWebSocketHandler, echo_options), |
744 | + (r'/ws', handlers.WebSocketHandler, ws_options), |
745 | + ]) |
746 | + |
747 | + def make_client(self): |
748 | + """Return a WebSocket client ready to be connected to the server.""" |
749 | + url = self.get_wss_url('/ws') |
750 | + # The client callback is tested elsewhere. |
751 | + callback = lambda message: None |
752 | + return helpers.WebSocketClient(url, callback, io_loop=self.io_loop) |
753 | + |
754 | + def make_handler(self): |
755 | + """Create and return a WebSocketHandler instance.""" |
756 | + request = mock.Mock() |
757 | + return handlers.WebSocketHandler(self.get_app(), request) |
758 | + |
759 | + @gen_test |
760 | + def test_initialization(self): |
761 | + # A WebSocket client is created and connected when the handler is |
762 | + # initialized. |
763 | + handler = self.make_handler() |
764 | + yield handler.initialize(self.echo_server_address) |
765 | + self.assertIsInstance(handler.jujuconn, clients.WebSocketClient) |
766 | + self.assertTrue(handler.jujuconn.connected) |
767 | + |
768 | + def test_client_callback(self): |
769 | + # The WebSocket client is created passing the proper callback. |
770 | + handler = self.make_handler() |
771 | + with mock.patch('guiserver.handlers.WebSocketClient') as mock_client: |
772 | + handler.initialize(self.echo_server_address) |
773 | + mock_client.assert_called_once_with( |
774 | + self.echo_server_address, handler.on_juju_message) |
775 | + |
776 | + def test_from_browser_to_juju(self): |
777 | + # A message from the browser is forwarded to the remote server. |
778 | + handler = self.make_handler() |
779 | + with mock.patch('guiserver.handlers.WebSocketClient'): |
780 | + handler.initialize(self.echo_server_address) |
781 | + handler.on_message('hello') |
782 | + handler.jujuconn.write_message.assert_called_once_with('hello') |
783 | + |
784 | + def test_from_juju_to_browser(self): |
785 | + # A message from the remote server is returned to the browser. |
786 | + handler = self.make_handler() |
787 | + handler.initialize(self.echo_server_address) |
788 | + with mock.patch('guiserver.handlers.WebSocketHandler.write_message'): |
789 | + handler.on_juju_message('hello') |
790 | + handler.write_message.assert_called_once_with('hello') |
791 | + |
792 | + @gen_test |
793 | + def test_end_to_end_proxy(self): |
794 | + # Messages are correctly forwarded from the client to the echo server |
795 | + # and back to the client. |
796 | + client = self.make_client() |
797 | + yield client.connect() |
798 | + message = yield client.send('hello') |
799 | + self.assertEqual('hello', message) |
800 | + |
801 | + @gen_test |
802 | + def test_connection_close(self): |
803 | + # The proxy connection is terminated when the client disconnects. |
804 | + client = self.make_client() |
805 | + yield client.connect() |
806 | + yield client.close() |
807 | + self.assertFalse(client.connected) |
808 | + yield self.echo_server_closed_future |
809 | + |
810 | + |
811 | +class TestIndexHandler(AsyncHTTPTestCase, LogTrapTestCase): |
812 | + |
813 | + def setUp(self): |
814 | + # Set up a static path with an index.html in it. |
815 | + self.path = tempfile.mkdtemp() |
816 | + self.addCleanup(shutil.rmtree, self.path) |
817 | + self.index_contents = 'We are the Borg!' |
818 | + index_path = os.path.join(self.path, 'index.html') |
819 | + with open(index_path, 'w') as index_file: |
820 | + index_file.write(self.index_contents) |
821 | + super(TestIndexHandler, self).setUp() |
822 | + |
823 | + def get_app(self): |
824 | + return web.Application([ |
825 | + (r'/(.*)', handlers.IndexHandler, {'path': self.path}), |
826 | + ]) |
827 | + |
828 | + def ensure_index(self, path): |
829 | + """Ensure the index contents are returned requesting the given path.""" |
830 | + response = self.fetch(path) |
831 | + self.assertEqual(200, response.code) |
832 | + self.assertEqual(self.index_contents, response.body) |
833 | + |
834 | + def test_root(self): |
835 | + # Requests for the root path are served by the index file. |
836 | + self.ensure_index('/') |
837 | + |
838 | + def test_page(self): |
839 | + # Requests for internal pages are served by the index file. |
840 | + self.ensure_index('/resistance/is/futile') |
841 | + |
842 | + def test_page_with_flags_and_queries(self): |
843 | + # Requests including flags and queries are served by the index file. |
844 | + self.ensure_index('/:flag:/activated/?my=query') |
845 | + |
846 | + |
847 | +class TestHttpsRedirectHandler(AsyncHTTPTestCase, LogTrapTestCase): |
848 | + |
849 | + def get_app(self): |
850 | + return web.Application([(r'.*', handlers.HttpsRedirectHandler)]) |
851 | + |
852 | + def assert_redirected(self, response, path): |
853 | + """Ensure the given response is a permanent redirect to the given path. |
854 | + |
855 | + Also check that the URL schema is HTTPS. |
856 | + """ |
857 | + self.assertEqual(301, response.code) |
858 | + expected = 'https://localhost:{}{}'.format(self.get_http_port(), path) |
859 | + self.assertEqual(expected, response.headers['location']) |
860 | + |
861 | + def test_redirection(self): |
862 | + # The HTTP traffic is redirected to HTTPS. |
863 | + response = self.fetch('/', follow_redirects=False) |
864 | + self.assert_redirected(response, '/') |
865 | + |
866 | + def test_page_redirection(self): |
867 | + # The path and query parts of the URL are preserved, |
868 | + path_and_query = '/my/page?my=query' |
869 | + response = self.fetch(path_and_query, follow_redirects=False) |
870 | + self.assert_redirected(response, path_and_query) |
871 | |
872 | === added file 'server/guiserver/tests/test_manage.py' |
873 | --- server/guiserver/tests/test_manage.py 1970-01-01 00:00:00 +0000 |
874 | +++ server/guiserver/tests/test_manage.py 2013-07-19 10:28:27 +0000 |
875 | @@ -0,0 +1,91 @@ |
876 | +# This file is part of the Juju GUI, which lets users view and manage Juju |
877 | +# environments within a graphical interface (https://launchpad.net/juju-gui). |
878 | +# Copyright (C) 2013 Canonical Ltd. |
879 | +# |
880 | +# This program is free software: you can redistribute it and/or modify it under |
881 | +# the terms of the GNU Affero General Public License version 3, as published by |
882 | +# the Free Software Foundation. |
883 | +# |
884 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
885 | +# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, |
886 | +# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
887 | +# Affero General Public License for more details. |
888 | +# |
889 | +# You should have received a copy of the GNU Affero General Public License |
890 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
891 | + |
892 | +"""Tests for the Juju GUI server management helpers.""" |
893 | + |
894 | +from contextlib import contextmanager |
895 | +import logging |
896 | +import unittest |
897 | + |
898 | +import mock |
899 | + |
900 | +from guiserver import manage |
901 | + |
902 | + |
903 | +@mock.patch('guiserver.manage.options') |
904 | +class TestAddDebug(unittest.TestCase): |
905 | + |
906 | + def test_debug_enabled(self, mock_options): |
907 | + # The debug option is true if the log level is debug. |
908 | + logger = mock.Mock(level=logging.DEBUG) |
909 | + manage._add_debug(logger) |
910 | + mock_options.define.assert_called_once_with('debug', default=True) |
911 | + |
912 | + def test_debug_disabled(self, mock_options): |
913 | + # The debug option is false if the log level is not debug. |
914 | + logger = mock.Mock(level=logging.INFO) |
915 | + manage._add_debug(logger) |
916 | + mock_options.define.assert_called_once_with('debug', default=False) |
917 | + |
918 | + |
919 | +class TestValidateRequired(unittest.TestCase): |
920 | + |
921 | + @contextmanager |
922 | + def assert_sysexit(self, option): |
923 | + """Ensure the code in the context manager block produces a system exit. |
924 | + |
925 | + Also check that the given error refers to the given option. |
926 | + """ |
927 | + expected_error = 'error: the {} argument is required'.format(option) |
928 | + with mock.patch('sys.exit') as mock_exit: |
929 | + yield |
930 | + mock_exit.assert_called_once_with(expected_error) |
931 | + |
932 | + def test_success(self): |
933 | + # The validation passes if the args are correctly found. |
934 | + with mock.patch('guiserver.manage.options', {'arg1': 'value1'}): |
935 | + manage._validate_required('arg1') |
936 | + |
937 | + def test_success_multiple_args(self): |
938 | + options = {'arg1': 'value1', 'arg2': 'value2'} |
939 | + with mock.patch('guiserver.manage.options', options): |
940 | + manage._validate_required(*options.keys()) |
941 | + |
942 | + def test_failure(self): |
943 | + with mock.patch('guiserver.manage.options', {'arg1': ''}): |
944 | + with self.assert_sysexit('arg1'): |
945 | + manage._validate_required('arg1') |
946 | + |
947 | + def test_failure_multiple_args(self): |
948 | + options = {'arg1': 'value1', 'arg2': ''} |
949 | + with mock.patch('guiserver.manage.options', options): |
950 | + with self.assert_sysexit('arg2'): |
951 | + manage._validate_required(*options.keys()) |
952 | + |
953 | + def test_failure_missing(self): |
954 | + with mock.patch('guiserver.manage.options', {'arg1': None}): |
955 | + with self.assert_sysexit('arg1'): |
956 | + manage._validate_required('arg1') |
957 | + |
958 | + def test_failure_empty(self): |
959 | + with mock.patch('guiserver.manage.options', {'arg1': ' '}): |
960 | + with self.assert_sysexit('arg1'): |
961 | + manage._validate_required('arg1') |
962 | + |
963 | + def test_failure_invalid_type(self): |
964 | + with mock.patch('guiserver.manage.options', {'arg1': 42}): |
965 | + with self.assert_sysexit('arg1'): |
966 | + manage._validate_required('arg1') |
967 | |
968 | === added file 'server/runserver.py' |
969 | --- server/runserver.py 1970-01-01 00:00:00 +0000 |
970 | +++ server/runserver.py 2013-07-19 10:28:27 +0000 |
971 | @@ -0,0 +1,29 @@ |
972 | +# This file is part of the Juju GUI, which lets users view and manage Juju |
973 | +# environments within a graphical interface (https://launchpad.net/juju-gui). |
974 | +# Copyright (C) 2013 Canonical Ltd. |
975 | +# |
976 | +# This program is free software: you can redistribute it and/or modify it under |
977 | +# the terms of the GNU Affero General Public License version 3, as published by |
978 | +# the Free Software Foundation. |
979 | +# |
980 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
981 | +# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, |
982 | +# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
983 | +# Affero General Public License for more details. |
984 | +# |
985 | +# You should have received a copy of the GNU Affero General Public License |
986 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
987 | + |
988 | +"""Juju GUI server entry point. |
989 | + |
990 | +Arguments example: |
991 | + --guiroot="/var/lib/juju/agents/unit-juju-gui-0/charm/juju-gui/build-prod" |
992 | + --jujuapi="wss://ec2-75-101-177-185.compute-1.example.com:17070" |
993 | +""" |
994 | + |
995 | +from guiserver import manage |
996 | + |
997 | + |
998 | +if __name__ == '__main__': |
999 | + manage.setup() |
1000 | + manage.run() |
1001 | |
1002 | === added file 'server/runtests.py' |
1003 | --- server/runtests.py 1970-01-01 00:00:00 +0000 |
1004 | +++ server/runtests.py 2013-07-19 10:28:27 +0000 |
1005 | @@ -0,0 +1,36 @@ |
1006 | +# This file is part of the Juju GUI, which lets users view and manage Juju |
1007 | +# environments within a graphical interface (https://launchpad.net/juju-gui). |
1008 | +# Copyright (C) 2013 Canonical Ltd. |
1009 | +# |
1010 | +# This program is free software: you can redistribute it and/or modify it under |
1011 | +# the terms of the GNU Affero General Public License version 3, as published by |
1012 | +# the Free Software Foundation. |
1013 | +# |
1014 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
1015 | +# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, |
1016 | +# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
1017 | +# Affero General Public License for more details. |
1018 | +# |
1019 | +# You should have received a copy of the GNU Affero General Public License |
1020 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
1021 | + |
1022 | + |
1023 | +"""Juju GUI server test suite entry point.""" |
1024 | + |
1025 | +import os |
1026 | +import unittest |
1027 | + |
1028 | +from tornado import testing |
1029 | + |
1030 | + |
1031 | +def all(): |
1032 | + """This is required by the Tornado test runner. |
1033 | + |
1034 | + See <http://www.tornadoweb.org/en/stable/testing.html#test-runner>. |
1035 | + """ |
1036 | + path = os.path.dirname(__file__) |
1037 | + return unittest.defaultTestLoader.discover(path) |
1038 | + |
1039 | + |
1040 | +if __name__ == '__main__': |
1041 | + testing.main(verbosity=2) |
1042 | |
1043 | === added file 'server/setup.py' |
1044 | --- server/setup.py 1970-01-01 00:00:00 +0000 |
1045 | +++ server/setup.py 2013-07-19 10:28:27 +0000 |
1046 | @@ -0,0 +1,53 @@ |
1047 | +# This file is part of the Juju GUI, which lets users view and manage Juju |
1048 | +# environments within a graphical interface (https://launchpad.net/juju-gui). |
1049 | +# Copyright (C) 2013 Canonical Ltd. |
1050 | +# |
1051 | +# This program is free software: you can redistribute it and/or modify it under |
1052 | +# the terms of the GNU Affero General Public License version 3, as published by |
1053 | +# the Free Software Foundation. |
1054 | +# |
1055 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
1056 | +# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, |
1057 | +# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
1058 | +# Affero General Public License for more details. |
1059 | +# |
1060 | +# You should have received a copy of the GNU Affero General Public License |
1061 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
1062 | + |
1063 | +"""Juju GUI server distribution file.""" |
1064 | + |
1065 | +from distutils.core import setup |
1066 | +import os |
1067 | + |
1068 | + |
1069 | +ROOT = os.path.abspath(os.path.dirname(__file__)) |
1070 | +PROJECT_NAME = 'guiserver' |
1071 | + |
1072 | +project = __import__(PROJECT_NAME) |
1073 | +readme_path = os.path.join(ROOT, '..', 'README.md') |
1074 | + |
1075 | +os.chdir(ROOT) |
1076 | +setup( |
1077 | + name=PROJECT_NAME, |
1078 | + version=project.get_version(), |
1079 | + description=project.__doc__, |
1080 | + long_description=open(readme_path).read(), |
1081 | + author='The Juju GUI team', |
1082 | + author_email='juju-gui@lists.ubuntu.com', |
1083 | + url='https://launchpad.net/juju-gui', |
1084 | + keywords='juju gui server', |
1085 | + packages=[ |
1086 | + PROJECT_NAME, |
1087 | + '{0}.tests'.format(PROJECT_NAME), |
1088 | + ], |
1089 | + scripts=['runserver.py'], |
1090 | + classifiers=[ |
1091 | + 'Development Status :: 3 - Alpha', |
1092 | + 'Environment :: Web Environment', |
1093 | + 'Intended Audience :: System Administrators', |
1094 | + 'License :: OSI Approved :: GNU Affero General Public License v3', |
1095 | + 'Operating System :: POSIX', |
1096 | + 'Programming Language :: Python', |
1097 | + 'Topic :: System :: Installation/Setup', |
1098 | + ], |
1099 | +) |
1100 | |
1101 | === added file 'tests/11-server.test' |
1102 | --- tests/11-server.test 1970-01-01 00:00:00 +0000 |
1103 | +++ tests/11-server.test 2013-07-19 10:28:27 +0000 |
1104 | @@ -0,0 +1,19 @@ |
1105 | +#!/bin/sh |
1106 | + |
1107 | +# This file is part of the Juju GUI, which lets users view and manage Juju |
1108 | +# environments within a graphical interface (https://launchpad.net/juju-gui). |
1109 | +# Copyright (C) 2012-2013 Canonical Ltd. |
1110 | +# |
1111 | +# This program is free software: you can redistribute it and/or modify it under |
1112 | +# the terms of the GNU Affero General Public License version 3, as published by |
1113 | +# the Free Software Foundation. |
1114 | +# |
1115 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
1116 | +# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, |
1117 | +# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
1118 | +# Affero General Public License for more details. |
1119 | +# |
1120 | +# You should have received a copy of the GNU Affero General Public License |
1121 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
1122 | + |
1123 | +tests/.venv/bin/python server/runtests.py |
1124 | |
1125 | === modified file 'tests/requirements.pip' |
1126 | --- tests/requirements.pip 2013-06-12 09:07:29 +0000 |
1127 | +++ tests/requirements.pip 2013-07-19 10:28:27 +0000 |
1128 | @@ -25,4 +25,6 @@ |
1129 | PyYAML==3.10 |
1130 | selenium==2.33.0 |
1131 | Tempita==0.5.1 |
1132 | +tornado==3.1 |
1133 | +ws4py==0.3.0-beta |
1134 | xvfbwrapper==0.2.2 |
Reviewers: mp+175624_ code.launchpad. net,
Message:
Please take a look.
Description:
Initial implementation of the Juju GUI server.
This first cut of the server replaces the
functionality provided in the charm by
haproxy and apache.
The server includes:
- static files serving;
- a WebSocket proxy handling the
connections between the browser and
the server and between the server and
the Juju API;
- HTTP to HTTPS redirecting.
Further features like auth and deployer
management will be added in the future.
ws4py is used to implement the WebSocket
client: there is something to fix there and
it will be done in a future branch.
At this time there is no easy way to QA the
server, but tests can be run using
`make unittest`: the test/lint suite is already
integrated with the one used by the charm.
Unfortunately the diff is quite long, sorry!
https:/ /code.launchpad .net/~frankban/ charms/ precise/ juju-gui/ server- base/+merge/ 175624
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/11530043/
Affected files: guiserver/ __init_ _.py guiserver/ apps.py guiserver/ clients. py guiserver/ handlers. py guiserver/ manage. py guiserver/ tests/_ _init__ .py guiserver/ tests/helpers. py guiserver/ tests/test_ clients. py guiserver/ tests/test_ handlers. py guiserver/ tests/test_ manage. py server. test nts.pip
M .bzrignore
M Dependencies.md
M Makefile
A Operation.md
M README.md
A [revision details]
M revision
A server/
A server/
A server/
A server/
A server/
A server/
A server/
A server/
A server/
A server/
A server/runserver.py
A server/runtests.py
A server/setup.py
A tests/11-
M tests/requireme