Merge lp:~frankban/charms/precise/juju-gui/server-base into lp:~juju-gui/charms/precise/juju-gui/trunk

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

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!

https://codereview.appspot.com/11530043/

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

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:
   M .bzrignore
   M Dependencies.md
   M Makefile
   A Operation.md
   M README.md
   A [revision details]
   M revision
   A server/guiserver/__init__.py
   A server/guiserver/apps.py
   A server/guiserver/clients.py
   A server/guiserver/handlers.py
   A server/guiserver/manage.py
   A server/guiserver/tests/__init__.py
   A server/guiserver/tests/helpers.py
   A server/guiserver/tests/test_clients.py
   A server/guiserver/tests/test_handlers.py
   A server/guiserver/tests/test_manage.py
   A server/runserver.py
   A server/runtests.py
   A server/setup.py
   A tests/11-server.test
   M tests/requirements.pip

Revision history for this message
Gary Poster (gary) wrote :

Wow, code LGTM. I'll try to run tests now...

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

https://codereview.appspot.com/11530043/diff/1/server/guiserver/apps.py#newcode32
server/guiserver/apps.py:32: # Avoid module level import so that options
can be properly set up.
Mm, so tornado has import side effects? :-/ too bad.

https://codereview.appspot.com/11530043/

Revision history for this message
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://codereview.appspot.com/11530043/diff/1/Operation.md
File Operation.md (right):

https://codereview.appspot.com/11530043/diff/1/Operation.md#newcode22
Operation.md:22: 443. HTTP connections to the 80 port are redirected to
the former one.
change to: connections to port 80 are

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

https://codereview.appspot.com/11530043/diff/1/server/guiserver/__init__.py#newcode28
server/guiserver/__init__.py:28: performs the actual orchestration work.
Both browser-server and server- Juju
Super trivial:

Remove the space: server-Juju

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

https://codereview.appspot.com/11530043/diff/1/server/guiserver/apps.py#newcode57
server/guiserver/apps.py:57: ], debug=options.debug)
I don't understand how this only redirects HTTP - HTTPS. Why doesn't
HTTPS traffic go to this handler?

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

https://codereview.appspot.com/11530043/diff/1/server/guiserver/clients.py#newcode35
server/guiserver/clients.py:35: new message is received by the client.
I'd say something about the optional *args and **kwargs. Not sure what.

https://codereview.appspot.com/11530043/diff/1/server/guiserver/clients.py#newcode61
server/guiserver/clients.py:61: # FIXME: find a way to avoid redifining
self.sock here.
typo: redefining

https://codereview.appspot.com/11530043/diff/1/server/guiserver/clients.py#newcode84
server/guiserver/clients.py:84: # FIXME: find a way to avoid redifining
self.sock here.
same typo

https://codereview.appspot.com/11530043/diff/1/server/guiserver/clients.py#newcode98
server/guiserver/clients.py:98: # FIXME: this seems clearly an error in
ws4py.
Maybe explain what you're referring to here.

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

https://codereview.appspot.com/11530043/diff/1/server/guiserver/manage.py#newcode78
server/guiserver/manage.py:78: redirector().listen(80)
Oh, there it is! NM.

https://codereview.appspot.com/11530043/diff/1/server/guiserver/tests/test_clients.py
File server/guiserver/tests/test_clients.py (right):

https://codereview.appspot.com/11530043/diff/1/server/guiserver/tests/test_clients.py#newcode60
server/guiserver/tests/test_clients.py:60:
Why does this test not use self.received like the next one does?

https://codereview.appspot.com/11530043/

94. By Francesco Banconi

Changes as per review.

95. By Francesco Banconi

Bumped revision up.

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

Please take a look.

https://codereview.appspot.com/11530043/diff/1/Operation.md
File Operation.md (right):

https://codereview.appspot.com/11530043/diff/1/Operation.md#newcode22
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://codereview.appspot.com/11530043/diff/1/server/guiserver/__init__.py
File server/guiserver/__init__.py (right):

https://codereview.appspot.com/11530043/diff/1/server/guiserver/__init__.py#newcode28
server/guiserver/__init__.py:28: performs the actual orchestration work.
Both browser-server and server- Juju
On 2013/07/18 21:52:56, bac wrote:
> Super trivial:

> Remove the space: server-Juju

Done.

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

https://codereview.appspot.com/11530043/diff/1/server/guiserver/apps.py#newcode32
server/guiserver/apps.py:32: # Avoid module level import so that options
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://codereview.appspot.com/11530043/diff/1/server/guiserver/clients.py
File server/guiserver/clients.py (right):

https://codereview.appspot.com/11530043/diff/1/server/guiserver/clients.py#newcode35
server/guiserver/clients.py:35: new message is received by the client.
On 2013/07/18 21:52:56, bac wrote:
> I'd say something about the optional *args and **kwargs. Not sure
what.

Done.

https://codereview.appspot.com/11530043/diff/1/server/guiserver/clients.py#newcode61
server/guiserver/clients.py:61: # FIXME: find a way to avoid redifining
self.sock here.
On 2013/07/18 21:52:56, bac wrote:
> typo: redefining

Done.

https://codereview.appspot.com/11530043/diff/1/server/guiserver/clients.py#newcode84
server/guiserver/clients.py:84: # FIXME: find a way to avoid redifining
self.sock here.
On 2013/07/18 21:52:56, bac wrote:
> same typo

Done.

https://codereview.appspot.com/11530043/diff/1/server/guiserver/clients.py#newcode98
server/guiserver/clients.py:98: # FIXME: this seems clearly an error in
ws4py.
On 2013/07/18 21:52:56, bac wrote:
> Maybe explain what you're referring to here.

Done.

https://codereview.appspot.com/11530043/diff/1/server/guiserver/tests/test_clients.py
File server/guiserver/tests/test_clients.py (right):

https://codereview.appspot.com/11530043/diff/1/server/guiserver/tests/test_clients.py#newcode60
server/guiserver/tests/test_clients.py:60:
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.append) is called passing the message.

https://codereview.appspot.com/11530043/

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

Thank you both for the reviews!

https://codereview.appspot.com/11530043/

Revision history for this message
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://codereview.appspot.com/11530043

https://codereview.appspot.com/11530043/

Revision history for this message
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://codereview.appspot.com/11530043

https://codereview.appspot.com/11530043/

Preview Diff

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

Subscribers

People subscribed via source and target branches