Merge lp:~teknico/charms/precise/juju-gui/integrate-builtin-server into lp:~juju-gui/charms/precise/juju-gui/trunk
- Precise Pangolin (12.04)
- integrate-builtin-server
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 89 |
Proposed branch: | lp:~teknico/charms/precise/juju-gui/integrate-builtin-server |
Merge into: | lp:~juju-gui/charms/precise/juju-gui/trunk |
Diff against target: |
726 lines (+246/-99) 11 files modified
Operation.md (+13/-2) config.yaml (+7/-0) config/guiserver.conf.template (+13/-0) config/haproxy.conf (+0/-22) hooks/backend.py (+28/-1) hooks/utils.py (+85/-30) revision (+1/-1) server/setup.py (+1/-1) tests/20-functional.test (+13/-2) tests/test_backends.py (+23/-15) tests/test_utils.py (+62/-25) |
To merge this branch: | bzr merge lp:~teknico/charms/precise/juju-gui/integrate-builtin-server |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
charmers | Pending | ||
Review via email: mp+177907@code.launchpad.net |
Commit message
Description of the change
Integrate the built-in server into the charm.
Add a "builtin-server" option to config.yaml, defaulting to false, to enable
a new Tornado-based built-in web server, in place of haproxy and Apache (still
the default).
Add a "guiserver.conf" Upstart config file, generated via a template.
Add a BuiltinServerMixin to hooks/backend.py .
Factor out common code betwenn HaproxyApacheMixin and BuiltinServerMixin.
Rename options.http to options.insecure in server/
Integrate testing of TestBackendCommands in TestBackendComm
Add a test for write_builtin_
Miscellaneuos cleanup.
Please QA by setting "builtin-server" to true in config.yaml and deploying
with "make deploy" (after bootstrapping), see "make help".
Nicola Larosa (teknico) wrote : | # |
- 89. By Nicola Larosa
-
Merge from trunk.
Nicola Larosa (teknico) wrote : | # |
Please take a look.
Gary Poster (gary) wrote : | # |
LGTM with changes.
Hi Nicola. This is very nice, thank you. I have some trivial changes
that you can take or leave, including a visit by the Grammar Police and
a few visits by the Idiosyncratic Text Rewriter.
I do have one change I feel strongly about atm, which you might disagree
with. I mutter about it somewhat incoherently below. I am going to try
and be clearer about it up here.
I think that the new upstart script should follow the haproxy upstart
pattern. Namely, it should be created during install, and copied over
in start(). We should also remove the copy, and the haproxy copy when
appropriate, in stop(), I think!
Otherwise, I think we will have problems when we switch from one server
to another. It will also be the right way forward when we think about
upgrading, I think.
I hope you agree, or don't disagree too much. :-)
Thanks!
Gary
https:/
File Operation.md (right):
https:/
Operation.md:40: removed, only the built-in server will remain.
removed. Only
or
removed, and only
or
removed; only
https:/
File config.yaml (right):
https:/
config.yaml:171: Enable the built-in server, disabling both haproxy and
Apache.
suggestion:
"This is a temporary option. The built-in server will be the only
server in the future."
(unless we want it to serve the sandbox?)
https:/
File hooks/backend.py (right):
https:/
hooks/backend.
Maybe add the following?
"""Mixins are not actually mixed in to the backend class using Python
inheritance machinery. Instead, each mixin is instantiated and
collected in the Backend __init__, as needed. Then the install(),
start(), and stop() methods have a "self" that is the simple
instantiated mixin, and a "backend" argument that is the backend
instance. Python inheritance machinery is somewhat mimicked in that
certain properties and methods are explicitly aggregated on the backend
instance: see the chain_methods function, the merge_properties method,
and their usages."""
<Gary procrastinates from writing doc he is supposed to be doing>
Composition is supposed to be simpler than inheritance, but I'm not sure
this entirely succeeded. My feeling is that it is because its
composition is still so similar in feel to inheritance. I think I might
be happier with this if, rather that Backend's
install = chain_methods(
we had a small change like this:
def install(self):
call_
where call_methods would be something like this.
def call_methods(objs, name, *args):
for obj in objs:
f = getattr(objs, name, None)
if f is not None:
and so on.
That would be clearer to me. I would be tempted to take it even
farther--make mixins dicts rather than classes, for instance, and values
currently using merge_properties could simply be calculated a...
- 90. By Nicola Larosa
-
Some doc and comment changes per gary's review.
Francesco Banconi (frankban) wrote : | # |
Thanks for this branch Nicola, good stuff.
I really appreciate your efforts in reorganizing the mixins/backends
stuff. Before starting to QA this, I'd like you to answer/address Gary's
comments and the ones I wrote below.
https:/
File config/
https:/
config/
--guiroot=
We could use the full path to runserver.py here.
https:/
config/
What do you think about always providing --apiversion and --sslpath? We
know those values in the hooks, and specifying them here allows for
removing these if blocks and also generates a more explicit config, i.e.
looking at the rendered upstart config we always know where are the ssl
certs and what juju implementation we are currently using.
https:/
File hooks/backend.py (right):
https:/
hooks/backend.
On 2013/07/31 21:18:18, gary.poster wrote:
> Composition is supposed to be simpler than inheritance, but I'm not
sure this
> entirely succeeded. My feeling is that it is because its composition
is still
> so similar in feel to inheritance. I think I might be happier with
this if,
> rather that Backend's
> install = chain_methods(
> we had a small change like this:
> def install(self):
> call_methods(
> where call_methods would be something like this.
> def call_methods(objs, name, *args):
> for obj in objs:
> f = getattr(objs, name, None)
> if f is not None:
> f(*args)
> and so on.
+1!
https:/
hooks/backend.
Thank you for reordering imports.
https:/
hooks/backend.
'go'
I think the definition of api_version can be moved inside the "if
config.
Anyway, is there a reason to prefer config.
over config[
https:/
hooks/backend.
On 2013/07/31 21:18:18, gary.poster wrote:
> For the change story, do you think we ought to remove the old
haproxy/apache
> startup files? I'm inclined to say yes, but open to alternate
opinions. :-)
> I guess that would happen in "stop"?
I agree. So, IIUC:
- the start method generates the upstart files;
- the stop method (called by the stop and by the config-changed hooks)
removes the upstart file(s) in use for that configuration.
https:/
hooks/backend.
utils.J...
Nicola Larosa (teknico) wrote : | # |
gary.poster wrote:
> Hi Nicola. This is very nice, thank you. I have some trivial changes
> that you can take or leave, including a visit by the Grammar Police
> and a few visits by the Idiosyncratic Text Rewriter.
Hi fellows, you're always welcome, make yourselves at home. Fancy some
tea? ;-)
> I think that the new upstart script should follow the haproxy upstart
> pattern. Namely, it should be created during install, and copied over
> in start(). We should also remove the copy, and the haproxy copy when
> appropriate, in stop(), I think!
> Otherwise, I think we will have problems when we switch from one
> server to another. It will also be the right way forward when we
> think about upgrading, I think.
Yes, that's a better strategy. See also frankban's clarification.
https:/
> Operation.md:40: removed, only the built-in server will remain.
> removed. Only
> or
> removed, and only
> or
> removed; only
Done; third option, I love semicolons.
> https:/
> File config.yaml (right):
https:/
> config.yaml:171: Enable the built-in server, disabling both haproxy
> and Apache. suggestion:
> "This is a temporary option. The built-in server will be the only
> server in the future."
Added.
> (unless we want it to serve the sandbox?)
Not sure what you mean here.
> https:/
> File hooks/backend.py (right):
https:/
> hooks/backend.
> Maybe add the following?
> """Mixins are not actually mixed in to the backend class using Python
> inheritance machinery. Instead, each mixin is instantiated and
> collected in the Backend __init__, as needed. Then the install(),
> start(), and stop() methods have a "self" that is the simple
> instantiated mixin, and a "backend" argument that is the backend
> instance. Python inheritance machinery is somewhat mimicked in that
> certain properties and methods are explicitly aggregated on the
> backend instance: see the chain_methods function, the merge_properties
> method, and their usages."""
Added.
> <Gary procrastinates from writing doc he is supposed to be doing>
That's ok. ;-)
> [rambling snipped]
> Anyway, just rambling. Nothing to do with this branch.
This actually points out something that's been nagging me for a while.
As discussed, the code structure has proved itself hard to read and
understand. I'll try your suggestions in a future branch and see how
much they improve matters.
https:/
> hooks/backend.
> Apache will go away
> a bit confusing.
> eventually this option will go away, as well as haproxy and Apache
> or similar?
Done.
https:/
> hooks/backend.
> For the change story, do you think we ought to remove the old
> hap...
Nicola Larosa (teknico) wrote : | # |
frankban wrote:
https:/
> config/
--guiroot=
> We could use the full path to runserver.py here.
To avoid possible interference with namesakes earlier in PATH. Done.
https:/
> config/
> What do you think about always providing --apiversion and --sslpath?
> We know those values in the hooks, and specifying them here allows for
> removing these if blocks and also generates a more explicit config,
> i.e. looking at the rendered upstart config we always know where are
> the ssl certs and what juju implementation we are currently using.
Done.
https:/
> hooks/backend.
> On 2013/07/31 21:18:18, gary.poster wrote:
>> Composition is supposed to be simpler than inheritance, but I'm not
>> sure this entirely succeeded. My feeling is that it is because its
>> composition is still so similar in feel to inheritance. I think I
>> might be happier with this if, rather that Backend's
>> [smip]
>> and so on.
> +1!
Agreed, I'll tackle this in a future branch, as mentioned in the reply
to gary's review.
https:/
> hooks/backend.
> Thank you for reordering imports.
One of those little things. :-)
https:/
> hooks/backend.
> else 'go'
> I think the definition of api_version can be moved inside the "if
> config.
Done.
> Anyway, is there a reason to prefer config.
False) over
> config[
Not really, as discussed I also removed other useless similar cases. The
only ones left are for SSL options without defaults.
https:/
> hooks/backend.
> On 2013/07/31 21:18:18, gary.poster wrote:
>> For the change story, do you think we ought to remove the old
>> haproxy/apache startup files? I'm inclined to say yes, but open to
>> alternate opinions. :-)
>> I guess that would happen in "stop"?
> I agree. So, IIUC:
> - the start method generates the upstart files;
> - the stop method (called by the stop and by the config-changed hooks)
> removes the upstart file(s) in use for that configuration.
Agreed, as mentioned in the reply to gary's review.
https:/
> hooks/backend.
> utils.JUJU_GUI_DIR is passed here and used in the function as
> gui_root. This does not seem to be correct. I expected "build_dir" to
> be the right value to pass here instead: am I missing something?
True, that was a thinko on my part, thank you.
> Moreover, utils.get_
- 91. By Nicola Larosa
-
More fixes from reviews.
- 92. By Nicola Larosa
-
Checkpoint.
- 93. By Nicola Larosa
-
Redone all install/start/stop logic, fixed tests, added functional test for builtin server.
- 94. By Nicola Larosa
-
Lint fix.
- 95. By Nicola Larosa
-
Reenable certificate tests.
- 96. By Nicola Larosa
-
Readd missing config/haproxy.conf file.
Nicola Larosa (teknico) wrote : | # |
After applying all the agreed changes the branch is very large (1200+
lines), and the functional tests are not yet passing. No use reproposing
now, see Launchpad for the current status.
The branch mixes two concerns: another refactoring of the
install/start/stop logic, to allow for the agreed upon changes, and the
actual integration of the builtin server.
It'll take a while more, but it seems more manageable to split these two
concerns into two different branches again.
- 97. By Nicola Larosa
-
Merge from trunk.
- 98. By Nicola Larosa
-
Merge from refactor-
config- generation- logic. - 99. By Nicola Larosa
-
Fix broken config.
- 100. By Nicola Larosa
-
Add installing the builtin server, fix test, some cleanup.
- 101. By Nicola Larosa
-
Fix config files and functional test.
- 102. By Nicola Larosa
-
Increase functional tests timeout.
- 103. By Nicola Larosa
-
Merge from refactor-
config- generation- logic. - 104. By Nicola Larosa
-
Add unit tests for the new functions in hooks/utils.py .
- 105. By Nicola Larosa
-
Clean up exports and path usage in tests.
- 106. By Nicola Larosa
-
Syntax error.
- 107. By Nicola Larosa
-
Merge from trunk via refactor-
config- generation- logic.
Preview Diff
1 | === modified file 'Operation.md' |
2 | --- Operation.md 2013-07-29 10:40:50 +0000 |
3 | +++ Operation.md 2013-08-08 09:20:50 +0000 |
4 | @@ -12,8 +12,8 @@ |
5 | ## How it works ## |
6 | |
7 | The Juju GUI is a client-side, JavaScript application that runs inside a |
8 | -web browser. The browser connects to a custom-made server deployed by |
9 | -the charm. |
10 | +web browser. The browser connects to a built-in server deployed by the |
11 | +charm. |
12 | |
13 | ## Server ## |
14 | |
15 | @@ -27,3 +27,14 @@ |
16 | Juju connections are bidirectional, using the WebSocket protocol on the |
17 | same port as the HTTPS connection, allowing changes in the Juju |
18 | environment to be propagated and shown immediately by the browser. |
19 | + |
20 | +## Activation ## |
21 | + |
22 | +Previously the Juju GUI has been served by a combination of haproxy and |
23 | +Apache, specifically deployed and configured by the charm. |
24 | + |
25 | +The new built-in server replaces them both and can be enabled by |
26 | +setting the config option `builtin-server` to `true`. |
27 | + |
28 | +In the future haproxy, Apache and the mentioned config option will be |
29 | +removed; only the built-in server will remain. |
30 | |
31 | === modified file 'config.yaml' |
32 | --- config.yaml 2013-08-05 09:38:39 +0000 |
33 | +++ config.yaml 2013-08-08 09:20:50 +0000 |
34 | @@ -166,3 +166,10 @@ |
35 | a link to juju.ubuntu.com |
36 | type: boolean |
37 | default: false |
38 | + builtin-server: |
39 | + description: | |
40 | + Enable the built-in server, disabling both haproxy and Apache. |
41 | + This is a temporary option: the built-in server will be |
42 | + the only server in the future. |
43 | + type: boolean |
44 | + default: false |
45 | |
46 | === added file 'config/guiserver.conf.template' |
47 | --- config/guiserver.conf.template 1970-01-01 00:00:00 +0000 |
48 | +++ config/guiserver.conf.template 2013-08-08 09:20:50 +0000 |
49 | @@ -0,0 +1,13 @@ |
50 | +description "GUIServer" |
51 | +author "Canonical" |
52 | + |
53 | +start on (filesystem and net-device-up IFACE=lo) |
54 | +stop on runlevel [!2345] |
55 | + |
56 | +exec /usr/bin/python /usr/local/bin/runserver.py \ |
57 | + --guiroot="{{gui_root}}" \ |
58 | + --apiurl="{{api_url}}" \ |
59 | + --apiversion="{{api_version}}" \ |
60 | + --sslpath="{{ssl_cert_path}}" \ |
61 | + {{if serve_tests}} --servetests \ {{endif}} |
62 | + {{if insecure}} --insecure {{endif}} |
63 | |
64 | === added file 'config/haproxy.conf' |
65 | --- config/haproxy.conf 1970-01-01 00:00:00 +0000 |
66 | +++ config/haproxy.conf 2013-08-08 09:20:50 +0000 |
67 | @@ -0,0 +1,22 @@ |
68 | +description "HAProxy" |
69 | +author "Canonical" |
70 | + |
71 | +start on (filesystem and net-device-up IFACE=lo) |
72 | +stop on runlevel [!2345] |
73 | + |
74 | +env CONF=/etc/haproxy/haproxy.cfg |
75 | +env DAEMON=/usr/sbin/haproxy |
76 | + |
77 | +expect fork |
78 | +respawn |
79 | +respawn limit 10 5 |
80 | + |
81 | +pre-start script |
82 | + # Test configuration, exit if errors are found. |
83 | + $DAEMON -c -f $CONF |
84 | + if [ $? -ne 0 ] |
85 | + then exit $? |
86 | + fi |
87 | +end script |
88 | + |
89 | +exec $DAEMON -f $CONF |
90 | |
91 | === removed file 'config/haproxy.conf' |
92 | --- config/haproxy.conf 2013-01-31 12:19:49 +0000 |
93 | +++ config/haproxy.conf 1970-01-01 00:00:00 +0000 |
94 | @@ -1,22 +0,0 @@ |
95 | -description "HAProxy" |
96 | -author "Canonical" |
97 | - |
98 | -start on (filesystem and net-device-up IFACE=lo) |
99 | -stop on runlevel [!2345] |
100 | - |
101 | -env CONF=/etc/haproxy/haproxy.cfg |
102 | -env DAEMON=/usr/sbin/haproxy |
103 | - |
104 | -expect fork |
105 | -respawn |
106 | -respawn limit 10 5 |
107 | - |
108 | -pre-start script |
109 | - # Test configuration, exit if errors are found. |
110 | - $DAEMON -c -f $CONF |
111 | - if [ $? -ne 0 ] |
112 | - then exit $? |
113 | - fi |
114 | -end script |
115 | - |
116 | -exec $DAEMON -f $CONF |
117 | |
118 | === added directory 'deps' |
119 | === added file 'deps/tornado-3.1.tar.gz' |
120 | Binary files deps/tornado-3.1.tar.gz 1970-01-01 00:00:00 +0000 and deps/tornado-3.1.tar.gz 2013-08-08 09:20:50 +0000 differ |
121 | === modified file 'hooks/backend.py' |
122 | --- hooks/backend.py 2013-08-05 09:59:17 +0000 |
123 | +++ hooks/backend.py 2013-08-08 09:20:50 +0000 |
124 | @@ -180,6 +180,28 @@ |
125 | utils.stop_haproxy_apache() |
126 | |
127 | |
128 | +class BuiltinServerMixin(ServerInstallMixinBase): |
129 | + """Manage the builtin server via Upstart.""" |
130 | + |
131 | + debs = ('openssl', 'python-pip') |
132 | + |
133 | + def install(self, backend): |
134 | + utils.install_tornado() |
135 | + utils.install_builtin_server() |
136 | + self._setup_certificates(backend) |
137 | + |
138 | + def start(self, backend): |
139 | + config = backend.config |
140 | + build_dir = utils.compute_build_dir( |
141 | + config['staging'], config['serve-tests']) |
142 | + utils.start_builtin_server( |
143 | + build_dir, config['serve-tests'], config['ssl-cert-path'], |
144 | + not config['secure']) |
145 | + |
146 | + def stop(self, backend): |
147 | + utils.stop_builtin_server() |
148 | + |
149 | + |
150 | def chain_methods(name): |
151 | """Helper to compose a set of mixin objects into a callable. |
152 | |
153 | @@ -249,7 +271,12 @@ |
154 | |
155 | # We always install and start the GUI. |
156 | self.mixins.append(GuiMixin()) |
157 | - self.mixins.append(HaproxyApacheMixin()) |
158 | + # TODO: eventually this option will go away, as well as haproxy and |
159 | + # Apache. |
160 | + if config['builtin-server']: |
161 | + self.mixins.append(BuiltinServerMixin()) |
162 | + else: |
163 | + self.mixins.append(HaproxyApacheMixin()) |
164 | |
165 | def different(self, *keys): |
166 | """Return a boolean indicating if the current config |
167 | |
168 | === modified file 'hooks/utils.py' |
169 | --- hooks/utils.py 2013-08-08 08:13:24 +0000 |
170 | +++ hooks/utils.py 2013-08-08 09:20:50 +0000 |
171 | @@ -17,19 +17,14 @@ |
172 | """Juju GUI charm utilities.""" |
173 | |
174 | __all__ = [ |
175 | - 'AGENT', |
176 | - 'APACHE', |
177 | 'APACHE_SITE', |
178 | 'APACHE_PORTS', |
179 | 'API_PORT', |
180 | 'CURRENT_DIR', |
181 | - 'HAPROXY', |
182 | - 'IMPROV', |
183 | 'JUJU_DIR', |
184 | 'JUJU_GUI_DIR', |
185 | 'JUJU_PEM', |
186 | 'WEB_PORT', |
187 | - 'bzr_checkout', |
188 | 'cmd_log', |
189 | 'compute_build_dir', |
190 | 'fetch_api', |
191 | @@ -53,9 +48,11 @@ |
192 | 'setup_gui', |
193 | 'setup_haproxy_config', |
194 | 'start_agent', |
195 | + 'start_builtin_server', |
196 | 'start_haproxy_apache', |
197 | 'start_improv', |
198 | 'stop_agent', |
199 | + 'stop_builtin_server', |
200 | 'stop_haproxy_apache', |
201 | 'stop_improv', |
202 | 'write_gui_config', |
203 | @@ -99,20 +96,32 @@ |
204 | |
205 | AGENT = 'juju-api-agent' |
206 | APACHE = 'apache2' |
207 | +BUILTIN_SERVER = 'guiserver' |
208 | +HAPROXY = 'haproxy' |
209 | IMPROV = 'juju-api-improv' |
210 | -HAPROXY = 'haproxy' |
211 | |
212 | API_PORT = 8080 |
213 | WEB_PORT = 8000 |
214 | |
215 | CURRENT_DIR = os.getcwd() |
216 | +CONFIG_DIR = os.path.join(CURRENT_DIR, 'config') |
217 | JUJU_DIR = os.path.join(CURRENT_DIR, 'juju') |
218 | JUJU_GUI_DIR = os.path.join(CURRENT_DIR, 'juju-gui') |
219 | -APACHE_SITE = '/etc/apache2/sites-available/juju-gui' |
220 | -APACHE_PORTS = '/etc/apache2/ports.conf' |
221 | -HAPROXY_PATH = '/etc/haproxy/haproxy.cfg' |
222 | -SYS_INIT_DIR = '/etc/init/' |
223 | -CONFIG_DIR = os.path.join(os.path.dirname(__file__), '..', 'config') |
224 | +SERVER_DIR = os.path.join(CURRENT_DIR, 'server') |
225 | +TORNADO_PATH = os.path.join(CURRENT_DIR, 'deps', 'tornado-3.1.tar.gz') |
226 | +UNIT_NAME = os.path.basename(os.path.realpath(os.path.join(CURRENT_DIR, '..'))) |
227 | + |
228 | +APACHE_CFG_DIR = os.path.join('', 'etc', 'apache2') |
229 | +APACHE_PORTS = os.path.join(APACHE_CFG_DIR, 'ports.conf') |
230 | +APACHE_SITE = os.path.join(APACHE_CFG_DIR, 'sites-available', 'juju-gui') |
231 | +HAPROXY_CFG_PATH = os.path.join('', 'etc', 'haproxy', 'haproxy.cfg') |
232 | + |
233 | +SYS_INIT_DIR = os.path.join('', 'etc', 'init') |
234 | +AGENT_INIT_PATH = os.path.join(SYS_INIT_DIR, 'juju-api-agent.conf') |
235 | +GUISERVER_INIT_PATH = os.path.join(SYS_INIT_DIR, 'guiserver.conf') |
236 | +HAPROXY_INIT_PATH = os.path.join(SYS_INIT_DIR, 'haproxy.conf') |
237 | +IMPROV_INIT_PATH = os.path.join(SYS_INIT_DIR, 'juju-api-improv.conf') |
238 | + |
239 | JUJU_PEM = 'juju.includes-private-key.pem' |
240 | DEB_BUILD_DEPENDENCIES = ( |
241 | 'bzr', 'g++', 'imagemagick', 'make', 'nodejs', 'npm', |
242 | @@ -120,7 +129,7 @@ |
243 | |
244 | |
245 | # Store the configuration from on invocation to the next. |
246 | -config_json = Serializer('/tmp/config.json') |
247 | +config_json = Serializer(os.path.join('', 'tmp', 'config.json')) |
248 | # Bazaar checkout command. |
249 | bzr_checkout = command('bzr', 'co', '--lightweight') |
250 | # Whether or not the charm is deployed using juju-core. |
251 | @@ -289,8 +298,7 @@ |
252 | The argument *destination* is a file path. |
253 | The argument *context* is a dict-like object. |
254 | """ |
255 | - template_path = os.path.join( |
256 | - os.path.dirname(__file__), '..', 'config', template_name) |
257 | + template_path = os.path.join(CONFIG_DIR, template_name) |
258 | template = tempita.Template.from_filename(template_path) |
259 | with open(destination, 'w') as stream: |
260 | stream.write(template.substitute(context)) |
261 | @@ -328,9 +336,7 @@ |
262 | 'port': API_PORT, |
263 | 'staging_env': staging_env, |
264 | } |
265 | - render_to_file( |
266 | - 'juju-api-improv.conf.template', context, |
267 | - '/etc/init/juju-api-improv.conf') |
268 | + render_to_file('juju-api-improv.conf.template', context, IMPROV_INIT_PATH) |
269 | log('Starting the staging backend.') |
270 | with su('root'): |
271 | service_control(IMPROV, START) |
272 | @@ -342,15 +348,13 @@ |
273 | with su('root'): |
274 | service_control(IMPROV, STOP) |
275 | log('Removing the staging Upstart script.') |
276 | - cmd_log(run('rm', '-f', '/etc/init/juju-api-improv.conf')) |
277 | + cmd_log(run('rm', '-f', IMPROV_INIT_PATH)) |
278 | |
279 | |
280 | def start_agent(ssl_cert_path, read_only=False): |
281 | """Start the Juju agent and connect to the current environment.""" |
282 | # Retrieve the Zookeeper address from the start up script. |
283 | - unit_dir = os.path.realpath(os.path.join(CURRENT_DIR, '..')) |
284 | - agent_file = os.path.join( |
285 | - SYS_INIT_DIR, 'juju-{}.conf'.format(os.path.basename(unit_dir))) |
286 | + agent_file = os.path.join(SYS_INIT_DIR, 'juju-{}.conf'.format(UNIT_NAME)) |
287 | zookeeper = get_zookeeper_address(agent_file) |
288 | log('Setting up the API agent Upstart script.') |
289 | context = { |
290 | @@ -360,9 +364,7 @@ |
291 | 'zookeeper': zookeeper, |
292 | 'read_only': read_only |
293 | } |
294 | - render_to_file( |
295 | - 'juju-api-agent.conf.template', context, |
296 | - '/etc/init/juju-api-agent.conf') |
297 | + render_to_file('juju-api-agent.conf.template', context, AGENT_INIT_PATH) |
298 | log('Starting the API agent.') |
299 | with su('root'): |
300 | service_control(AGENT, START) |
301 | @@ -374,7 +376,7 @@ |
302 | with su('root'): |
303 | service_control(AGENT, STOP) |
304 | log('Removing the API agent Upstart script.') |
305 | - cmd_log(run('rm', '-f', '/etc/init/juju-api-agent.conf')) |
306 | + cmd_log(run('rm', '-f', AGENT_INIT_PATH)) |
307 | |
308 | |
309 | def compute_build_dir(in_staging, serve_tests): |
310 | @@ -442,7 +444,7 @@ |
311 | is_legacy_juju = legacy_juju() |
312 | if is_legacy_juju: |
313 | # The PyJuju API agent is listening on localhost. |
314 | - api_address = '127.0.0.1:{0}'.format(API_PORT) |
315 | + api_address = '127.0.0.1:{}'.format(API_PORT) |
316 | else: |
317 | # Retrieve the juju-core API server address. |
318 | api_address = get_api_address() |
319 | @@ -458,15 +460,14 @@ |
320 | 'web_port': WEB_PORT, |
321 | 'secure': secure |
322 | } |
323 | - render_to_file('haproxy.cfg.template', context, HAPROXY_PATH) |
324 | + render_to_file('haproxy.cfg.template', context, HAPROXY_CFG_PATH) |
325 | |
326 | |
327 | def remove_haproxy_setup(): |
328 | """Remove haproxy setup.""" |
329 | log('Removing haproxy setup.') |
330 | - cmd_log(run('rm', '-f', HAPROXY_PATH)) |
331 | - config_path = os.path.join(SYS_INIT_DIR, 'haproxy.conf') |
332 | - cmd_log(run('rm', '-f', config_path)) |
333 | + cmd_log(run('rm', '-f', HAPROXY_CFG_PATH)) |
334 | + cmd_log(run('rm', '-f', HAPROXY_INIT_PATH)) |
335 | |
336 | |
337 | def setup_apache_config(build_dir, serve_tests=False): |
338 | @@ -523,6 +524,60 @@ |
339 | remove_apache_setup() |
340 | |
341 | |
342 | +def install_tornado(): |
343 | + """Install Tornado from a local tarball.""" |
344 | + log('Installing Tornado.') |
345 | + with su('root'): |
346 | + cmd_log(run('pip', 'install', TORNADO_PATH)) |
347 | + |
348 | + |
349 | +def install_builtin_server(): |
350 | + """Install the builtin server code.""" |
351 | + log('Installing the builtin server.') |
352 | + setup_cmd = os.path.join(SERVER_DIR, 'setup.py') |
353 | + with su('root'): |
354 | + cmd_log(run('/usr/bin/python', setup_cmd, 'install')) |
355 | + |
356 | + |
357 | +def write_builtin_server_startup( |
358 | + gui_root, ssl_cert_path, serve_tests=False, insecure=False): |
359 | + """Generate the builtin server Upstart file.""" |
360 | + log('Generating the builtin server Upstart file.') |
361 | + url_prefix = 'ws' if insecure else 'wss' |
362 | + is_legacy_juju = legacy_juju() |
363 | + api_address = get_api_address() |
364 | + api_url = '{}://127.0.0.1:{}'.format( |
365 | + url_prefix, API_PORT) if is_legacy_juju else '{}://{}'.format( |
366 | + url_prefix, api_address) |
367 | + context = { |
368 | + 'gui_root': gui_root, |
369 | + 'api_url': api_url, |
370 | + 'api_version': 'python' if is_legacy_juju else 'go', |
371 | + 'ssl_cert_path': ssl_cert_path, |
372 | + 'serve_tests': serve_tests, |
373 | + 'insecure': insecure, |
374 | + } |
375 | + render_to_file( |
376 | + 'guiserver.conf.template', context, GUISERVER_INIT_PATH) |
377 | + |
378 | + |
379 | +def start_builtin_server(build_dir, serve_tests, ssl_cert_path, insecure): |
380 | + """Start the builtin server.""" |
381 | + write_builtin_server_startup( |
382 | + build_dir, ssl_cert_path, serve_tests, insecure) |
383 | + log('Starting the builtin server.') |
384 | + with su('root'): |
385 | + service_control(BUILTIN_SERVER, RESTART) |
386 | + |
387 | + |
388 | +def stop_builtin_server(): |
389 | + """Stop the builtin server.""" |
390 | + log('Stopping the builtin server.') |
391 | + with su('root'): |
392 | + service_control(BUILTIN_SERVER, STOP) |
393 | + cmd_log(run('rm', '-f', GUISERVER_INIT_PATH)) |
394 | + |
395 | + |
396 | def get_npm_cache_archive_url(Launchpad=Launchpad): |
397 | """Figure out the URL of the most recent NPM cache archive on Launchpad.""" |
398 | launchpad = Launchpad.login_anonymously('Juju GUI charm', 'production') |
399 | |
400 | === modified file 'revision' |
401 | --- revision 2013-08-05 15:00:27 +0000 |
402 | +++ revision 2013-08-08 09:20:50 +0000 |
403 | @@ -1,1 +1,1 @@ |
404 | -66 |
405 | +67 |
406 | |
407 | === modified file 'server/setup.py' |
408 | --- server/setup.py 2013-07-17 15:20:47 +0000 |
409 | +++ server/setup.py 2013-08-08 09:20:50 +0000 |
410 | @@ -38,7 +38,7 @@ |
411 | keywords='juju gui server', |
412 | packages=[ |
413 | PROJECT_NAME, |
414 | - '{0}.tests'.format(PROJECT_NAME), |
415 | + '{}.tests'.format(PROJECT_NAME), |
416 | ], |
417 | scripts=['runserver.py'], |
418 | classifiers=[ |
419 | |
420 | === modified file 'tests/20-functional.test' |
421 | --- tests/20-functional.test 2013-08-05 15:00:27 +0000 |
422 | +++ tests/20-functional.test 2013-08-08 09:20:50 +0000 |
423 | @@ -82,7 +82,7 @@ |
424 | Retry loading the page until the page is found or a timeout exception |
425 | is raised. |
426 | """ |
427 | - base_url = 'https://{0}:{1}'.format(hostname, self.port) |
428 | + base_url = 'https://{}:{}'.format(hostname, self.port) |
429 | url = urlparse.urljoin(base_url, path) |
430 | |
431 | def page_ready(driver): |
432 | @@ -134,7 +134,7 @@ |
433 | # started by the charm in the machine. Right now this does not |
434 | # work in pyJuju, so the desired effect is achieved by keeping |
435 | # track of started services and manually stopping them here. |
436 | - target = 'ubuntu@{0}'.format(hostname) |
437 | + target = 'ubuntu@{}'.format(hostname) |
438 | for service in services: |
439 | ssh(target, 'sudo', 'service', service, 'stop') |
440 | |
441 | @@ -177,6 +177,17 @@ |
442 | # The staging environment contains five deployed services. |
443 | self.assertSetEqual(set(STAGING_SERVICES), self.get_service_names()) |
444 | |
445 | + def test_builtin_server(self): |
446 | + # Ensure the Juju GUI and builtin server are correctly set up. |
447 | + unit_info = self.juju_deploy( |
448 | + self.charm, options={'builtin-server': 'true'}) |
449 | + hostname = unit_info['public-address'] |
450 | + conn = httplib.HTTPSConnection(hostname) |
451 | + conn.request('HEAD', '/') |
452 | + headers = conn.getresponse().getheaders() |
453 | + server_header = dict(headers)['server'] |
454 | + self.assertIn('TornadoServer', server_header) |
455 | + |
456 | def test_branch_source(self): |
457 | # Ensure the Juju GUI is correctly deployed from a Bazaar branch. |
458 | unit_info = self.juju_deploy( |
459 | |
460 | === modified file 'tests/test_backends.py' |
461 | --- tests/test_backends.py 2013-08-05 09:59:17 +0000 |
462 | +++ tests/test_backends.py 2013-08-08 09:20:50 +0000 |
463 | @@ -46,8 +46,8 @@ |
464 | """Ensure the correct mixins and property values are collected.""" |
465 | |
466 | def test_staging_backend(self): |
467 | - test_backend = backend.Backend( |
468 | - config={'sandbox': False, 'staging': True}) |
469 | + test_backend = backend.Backend(config={ |
470 | + 'sandbox': False, 'staging': True, 'builtin-server': False}) |
471 | mixin_names = get_mixin_names(test_backend) |
472 | self.assertEqual( |
473 | ('ImprovMixin', 'GuiMixin', 'HaproxyApacheMixin'), |
474 | @@ -57,8 +57,8 @@ |
475 | test_backend.debs) |
476 | |
477 | def test_sandbox_backend(self): |
478 | - test_backend = backend.Backend( |
479 | - config={'sandbox': True, 'staging': False}) |
480 | + test_backend = backend.Backend(config={ |
481 | + 'sandbox': True, 'staging': False, 'builtin-server': False}) |
482 | mixin_names = get_mixin_names(test_backend) |
483 | self.assertEqual( |
484 | ('SandboxMixin', 'GuiMixin', 'HaproxyApacheMixin'), |
485 | @@ -68,8 +68,8 @@ |
486 | test_backend.debs) |
487 | |
488 | def test_python_backend(self): |
489 | - test_backend = backend.Backend( |
490 | - config={'sandbox': False, 'staging': False}) |
491 | + test_backend = backend.Backend(config={ |
492 | + 'sandbox': False, 'staging': False, 'builtin-server': False}) |
493 | mixin_names = get_mixin_names(test_backend) |
494 | self.assertEqual( |
495 | ('PythonMixin', 'GuiMixin', 'HaproxyApacheMixin'), |
496 | @@ -86,8 +86,8 @@ |
497 | # Create a fake agent file. |
498 | agent_path = os.path.join(base_dir, 'agent.conf') |
499 | open(agent_path, 'w').close() |
500 | - test_backend = backend.Backend( |
501 | - config={'sandbox': False, 'staging': False}) |
502 | + test_backend = backend.Backend(config={ |
503 | + 'sandbox': False, 'staging': False, 'builtin-server': False}) |
504 | # Cleanup. |
505 | utils.CURRENT_DIR = orig_current_dir |
506 | shutil.rmtree(base_dir) |
507 | @@ -118,6 +118,8 @@ |
508 | 'find_missing_packages': utils.find_missing_packages, |
509 | 'get_api_address': utils.get_api_address, |
510 | 'get_npm_cache_archive_url': utils.get_npm_cache_archive_url, |
511 | + 'install_builtin_server': utils.install_builtin_server, |
512 | + 'install_tornado': utils.install_tornado, |
513 | 'parse_source': utils.parse_source, |
514 | 'prime_npm_cache': utils.prime_npm_cache, |
515 | 'remove_apache_setup': utils.remove_apache_setup, |
516 | @@ -128,6 +130,7 @@ |
517 | 'setup_haproxy_config': utils.setup_haproxy_config, |
518 | 'start_agent': utils.start_agent, |
519 | 'start_improv': utils.start_improv, |
520 | + 'write_builtin_server_startup': utils.write_builtin_server_startup, |
521 | 'write_gui_config': utils.write_gui_config, |
522 | } |
523 | self.charmhelpers_mocks = { |
524 | @@ -192,11 +195,12 @@ |
525 | self.assertTrue( |
526 | self.called.get(mocked), '{} was not called'.format(mocked)) |
527 | |
528 | - def test_install_improv(self): |
529 | + def test_install_improv_builtin(self): |
530 | test_backend = backend.Backend(config=self.alwaysTrue) |
531 | test_backend.install() |
532 | for mocked in ( |
533 | 'apt_get_install', 'fetch_api', 'find_missing_packages', |
534 | + 'install_builtin_server', 'install_tornado', |
535 | ): |
536 | self.assertTrue( |
537 | self.called.get(mocked), '{} was not called'.format(mocked)) |
538 | @@ -211,12 +215,12 @@ |
539 | self.assertTrue( |
540 | self.called.get(mocked), '{} was not called'.format(mocked)) |
541 | |
542 | - def test_start_improv(self): |
543 | + def test_start_improv_builtin(self): |
544 | test_backend = backend.Backend(config=self.alwaysTrue) |
545 | test_backend.start() |
546 | for mocked in ( |
547 | 'compute_build_dir', 'open_port', 'start_improv', 'su', |
548 | - 'write_gui_config', |
549 | + 'write_builtin_server_startup', 'write_gui_config', |
550 | ): |
551 | self.assertTrue( |
552 | self.called.get(mocked), '{} was not called'.format(mocked)) |
553 | @@ -231,16 +235,20 @@ |
554 | |
555 | def test_same_config(self): |
556 | test_backend = backend.Backend( |
557 | - config={'sandbox': False, 'staging': False}, |
558 | - prev_config={'sandbox': False, 'staging': False}, |
559 | + config={ |
560 | + 'sandbox': False, 'staging': False, 'builtin-server': False}, |
561 | + prev_config={ |
562 | + 'sandbox': False, 'staging': False, 'builtin-server': False}, |
563 | ) |
564 | self.assertFalse(test_backend.different('sandbox')) |
565 | self.assertFalse(test_backend.different('staging')) |
566 | |
567 | def test_different_config(self): |
568 | test_backend = backend.Backend( |
569 | - config={'sandbox': False, 'staging': False}, |
570 | - prev_config={'sandbox': True, 'staging': False}, |
571 | + config={ |
572 | + 'sandbox': False, 'staging': False, 'builtin-server': False}, |
573 | + prev_config={ |
574 | + 'sandbox': True, 'staging': False, 'builtin-server': False}, |
575 | ) |
576 | self.assertTrue(test_backend.different('sandbox')) |
577 | self.assertFalse(test_backend.different('staging')) |
578 | |
579 | === modified file 'tests/test_utils.py' |
580 | --- tests/test_utils.py 2013-08-07 09:50:50 +0000 |
581 | +++ tests/test_utils.py 2013-08-08 09:20:50 +0000 |
582 | @@ -45,6 +45,8 @@ |
583 | log_hook, |
584 | parse_source, |
585 | get_npm_cache_archive_url, |
586 | + install_builtin_server, |
587 | + install_tornado, |
588 | remove_apache_setup, |
589 | remove_haproxy_setup, |
590 | render_to_file, |
591 | @@ -52,11 +54,14 @@ |
592 | setup_apache_config, |
593 | setup_haproxy_config, |
594 | start_agent, |
595 | + start_builtin_server, |
596 | start_haproxy_apache, |
597 | start_improv, |
598 | stop_agent, |
599 | + stop_builtin_server, |
600 | stop_haproxy_apache, |
601 | stop_improv, |
602 | + write_builtin_server_startup, |
603 | write_gui_config, |
604 | ) |
605 | # Import the whole utils package for monkey patching. |
606 | @@ -379,7 +384,7 @@ |
607 | |
608 | def setUp(self): |
609 | self.zookeeper_address = 'example.com:2000' |
610 | - contents = 'env JUJU_ZOOKEEPER="{0}"\n'.format(self.zookeeper_address) |
611 | + contents = 'env JUJU_ZOOKEEPER="{}"\n'.format(self.zookeeper_address) |
612 | with tempfile.NamedTemporaryFile(delete=False) as agent_file: |
613 | agent_file.write(contents) |
614 | self.agent_file_path = agent_file.name |
615 | @@ -674,30 +679,15 @@ |
616 | result, build_dir, 'in_staging: {}, serve_tests: {}'.format( |
617 | in_staging, serve_tests)) |
618 | |
619 | - def test_write_gui_config(self): |
620 | - write_gui_config( |
621 | - False, 'This is login help.', True, True, self.charmworld_url, |
622 | - self.build_dir, use_analytics=True, config_js_path='config') |
623 | - js_conf = self.files['config'] |
624 | - self.assertIn('consoleEnabled: false', js_conf) |
625 | - self.assertIn('user: "admin"', js_conf) |
626 | - self.assertIn('password: "admin"', js_conf) |
627 | - self.assertIn('login_help: "This is login help."', js_conf) |
628 | - self.assertIn('readOnly: true', js_conf) |
629 | - self.assertIn("socket_url: 'wss://", js_conf) |
630 | - self.assertIn('socket_protocol: "wss"', js_conf) |
631 | - self.assertIn('charmworldURL: "http://charmworld.example"', js_conf) |
632 | - self.assertIn('useAnalytics: true', js_conf) |
633 | - |
634 | def test_setup_haproxy_config(self): |
635 | setup_haproxy_config(self.ssl_cert_path) |
636 | haproxy_conf = self.files['haproxy.cfg'] |
637 | - self.assertIn('ca-base {0}'.format(self.ssl_cert_path), haproxy_conf) |
638 | - self.assertIn('crt-base {0}'.format(self.ssl_cert_path), haproxy_conf) |
639 | - self.assertIn('ws1 127.0.0.1:{0}'.format(API_PORT), haproxy_conf) |
640 | - self.assertIn('web1 127.0.0.1:{0}'.format(WEB_PORT), haproxy_conf) |
641 | - self.assertIn('ca-file {0}'.format(JUJU_PEM), haproxy_conf) |
642 | - self.assertIn('crt {0}'.format(JUJU_PEM), haproxy_conf) |
643 | + self.assertIn('ca-base {}'.format(self.ssl_cert_path), haproxy_conf) |
644 | + self.assertIn('crt-base {}'.format(self.ssl_cert_path), haproxy_conf) |
645 | + self.assertIn('ws1 127.0.0.1:{}'.format(API_PORT), haproxy_conf) |
646 | + self.assertIn('web1 127.0.0.1:{}'.format(WEB_PORT), haproxy_conf) |
647 | + self.assertIn('ca-file {}'.format(JUJU_PEM), haproxy_conf) |
648 | + self.assertIn('crt {}'.format(JUJU_PEM), haproxy_conf) |
649 | self.assertIn('redirect scheme https', haproxy_conf) |
650 | |
651 | def test_remove_haproxy_setup(self): |
652 | @@ -708,9 +698,9 @@ |
653 | setup_apache_config(self.build_dir, serve_tests=True) |
654 | apache_site_conf = self.files['SITE_NOT_THERE'] |
655 | self.assertIn('juju-gui/build-', apache_site_conf) |
656 | - self.assertIn('VirtualHost *:{0}'.format(WEB_PORT), apache_site_conf) |
657 | + self.assertIn('VirtualHost *:{}'.format(WEB_PORT), apache_site_conf) |
658 | self.assertIn( |
659 | - 'Alias /test {0}/test/'.format(JUJU_GUI_DIR), apache_site_conf) |
660 | + 'Alias /test {}/test/'.format(JUJU_GUI_DIR), apache_site_conf) |
661 | apache_ports_conf = self.files['PORTS_NOT_THERE'] |
662 | self.assertIn('NameVirtualHost *:8000', apache_ports_conf) |
663 | self.assertIn('Listen 8000', apache_ports_conf) |
664 | @@ -720,7 +710,7 @@ |
665 | self.assertEqual(self.run_call_count, 3) |
666 | |
667 | def test_start_haproxy_apache(self): |
668 | - start_haproxy_apache('build_dir', False, self.ssl_cert_path, True) |
669 | + start_haproxy_apache(JUJU_GUI_DIR, False, self.ssl_cert_path, True) |
670 | self.assertEqual(self.svc_ctl_call_count, 2) |
671 | self.assertEqual(self.service_names, ['apache2', 'haproxy']) |
672 | self.assertEqual( |
673 | @@ -732,6 +722,53 @@ |
674 | self.assertEqual(self.service_names, ['haproxy', 'apache2']) |
675 | self.assertEqual(self.actions, [charmhelpers.STOP, charmhelpers.STOP]) |
676 | |
677 | + def test_install_tornado(self): |
678 | + install_tornado() |
679 | + self.assertEqual(self.run_call_count, 1) |
680 | + |
681 | + def test_install_builtin_server(self): |
682 | + install_builtin_server() |
683 | + self.assertEqual(self.run_call_count, 1) |
684 | + |
685 | + def test_write_builtin_server_startup(self): |
686 | + write_builtin_server_startup( |
687 | + JUJU_GUI_DIR, self.ssl_cert_path, serve_tests=True, insecure=True) |
688 | + guiserver_conf = self.files['guiserver.conf'] |
689 | + self.assertIn('description "GUIServer"', guiserver_conf) |
690 | + self.assertIn('--apiurl="ws://127.0.0.1:8080"', guiserver_conf) |
691 | + self.assertIn('--apiversion="python"', guiserver_conf) |
692 | + self.assertIn('--servetests', guiserver_conf) |
693 | + self.assertIn('--insecure', guiserver_conf) |
694 | + |
695 | + def test_start_builtin_server(self): |
696 | + start_builtin_server( |
697 | + JUJU_GUI_DIR, False, self.ssl_cert_path, insecure=False) |
698 | + self.assertEqual(self.svc_ctl_call_count, 1) |
699 | + self.assertEqual(self.service_names, ['guiserver']) |
700 | + self.assertEqual(self.actions, [charmhelpers.RESTART]) |
701 | + |
702 | + def test_stop_builtin_server(self): |
703 | + stop_builtin_server() |
704 | + self.assertEqual(self.svc_ctl_call_count, 1) |
705 | + self.assertEqual(self.service_names, ['guiserver']) |
706 | + self.assertEqual(self.actions, [charmhelpers.STOP]) |
707 | + self.assertEqual(self.run_call_count, 1) |
708 | + |
709 | + def test_write_gui_config(self): |
710 | + write_gui_config( |
711 | + False, 'This is login help.', True, True, self.charmworld_url, |
712 | + self.build_dir, use_analytics=True, config_js_path='config') |
713 | + js_conf = self.files['config'] |
714 | + self.assertIn('consoleEnabled: false', js_conf) |
715 | + self.assertIn('user: "admin"', js_conf) |
716 | + self.assertIn('password: "admin"', js_conf) |
717 | + self.assertIn('login_help: "This is login help."', js_conf) |
718 | + self.assertIn('readOnly: true', js_conf) |
719 | + self.assertIn("socket_url: 'wss://", js_conf) |
720 | + self.assertIn('socket_protocol: "wss"', js_conf) |
721 | + self.assertIn('charmworldURL: "http://charmworld.example"', js_conf) |
722 | + self.assertIn('useAnalytics: true', js_conf) |
723 | + |
724 | def test_write_gui_config_insecure(self): |
725 | write_gui_config( |
726 | False, 'This is login help.', True, True, self.charmworld_url, |
Reviewers: mp+177907_ code.launchpad. net,
Message:
Please take a look.
Description:
Integrate the built-in server into the charm.
Add a "builtin-server" option to config.yaml, defaulting to false, to
enable
a new Tornado-based built-in web server, in place of haproxy and Apache
(still
the default).
Add a "guiserver.conf" Upstart config file, generated via a template.
Add a BuiltinServerMixin to hooks/backend.py .
Factor out common code betwenn HaproxyApacheMixin and
BuiltinServerMixin.
Rename options.http to options.insecure in server/ guiserver/ manage. py .
Integrate testing of TestBackendCommands in TestBackendComm ands.
Add a test for write_builtin_ server_ startup.
Miscellaneuos cleanup.
Please QA by setting "builtin-server" to true in config.yaml and
deploying
with "make deploy" (after bootstrapping), see "make help".
https:/ /code.launchpad .net/~teknico/ charms/ precise/ juju-gui/ integrate- builtin- server/ +merge/ 177907
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/12086044/
Affected files: guiserver. conf.template 3.1.tar. gz guiserver/ manage. py guiserver/ tests/test_ manage. py backends. py
M Operation.md
A [revision details]
M config.yaml
A config/
A deps/tornado-
M hooks/backend.py
M hooks/utils.py
M server/
M server/
M server/runserver.py
M tests/test_
M tests/test_utils.py