Merge lp:~teknico/charms/precise/juju-gui/integrate-builtin-server into lp:~juju-gui/charms/precise/juju-gui/trunk

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

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/guiserver/manage.py .

Integrate testing of TestBackendCommands in TestBackendCommands.

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://codereview.appspot.com/12086044/

To post a comment you must log in.
Revision history for this message
Nicola Larosa (teknico) wrote :

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 TestBackendCommands.

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:
   M Operation.md
   A [revision details]
   M config.yaml
   A config/guiserver.conf.template
   A deps/tornado-3.1.tar.gz
   M hooks/backend.py
   M hooks/utils.py
   M server/guiserver/manage.py
   M server/guiserver/tests/test_manage.py
   M server/runserver.py
   M tests/test_backends.py
   M tests/test_utils.py

89. By Nicola Larosa

Merge from trunk.

Revision history for this message
Nicola Larosa (teknico) wrote :
Revision history for this message
Gary Poster (gary) wrote :
Download full text (6.1 KiB)

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://codereview.appspot.com/12086044/diff/4001/Operation.md
File Operation.md (right):

https://codereview.appspot.com/12086044/diff/4001/Operation.md#newcode40
Operation.md:40: removed, only the built-in server will remain.
removed. Only

or

removed, and only

or

removed; only

https://codereview.appspot.com/12086044/diff/4001/config.yaml
File config.yaml (right):

https://codereview.appspot.com/12086044/diff/4001/config.yaml#newcode171
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://codereview.appspot.com/12086044/diff/4001/hooks/backend.py
File hooks/backend.py (right):

https://codereview.appspot.com/12086044/diff/4001/hooks/backend.py#newcode24
hooks/backend.py:24:
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('install')

we had a small change like this:

def install(self):
     call_methods(self.mixins, 'install', self)

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.

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...

Read more...

90. By Nicola Larosa

Some doc and comment changes per gary's review.

Revision history for this message
Francesco Banconi (frankban) wrote :
Download full text (5.4 KiB)

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://codereview.appspot.com/12086044/diff/4001/config/guiserver.conf.template
File config/guiserver.conf.template (right):

https://codereview.appspot.com/12086044/diff/4001/config/guiserver.conf.template#newcode7
config/guiserver.conf.template:7: exec runserver.py
--guiroot="{{gui_root}}" \
We could use the full path to runserver.py here.

https://codereview.appspot.com/12086044/diff/4001/config/guiserver.conf.template#newcode9
config/guiserver.conf.template:9: {{if api_version}}
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://codereview.appspot.com/12086044/diff/4001/hooks/backend.py
File hooks/backend.py (right):

https://codereview.appspot.com/12086044/diff/4001/hooks/backend.py#newcode24
hooks/backend.py:24:
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('install')

> we had a small change like this:

> def install(self):
> call_methods(self.mixins, 'install', self)

> 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://codereview.appspot.com/12086044/diff/4001/hooks/backend.py#newcode35
hooks/backend.py:35: import charmhelpers
Thank you for reordering imports.

https://codereview.appspot.com/12086044/diff/4001/hooks/backend.py#newcode140
hooks/backend.py:140: api_version = 'python' if utils.legacy_juju() else
'go'
I think the definition of api_version can be moved inside the "if
config.get('builtin-server', False)" block below.
Anyway, is there a reason to prefer config.get('builtin-server', False)
over config['builtin-server']?

https://codereview.appspot.com/12086044/diff/4001/hooks/backend.py#newcode141
hooks/backend.py:141: if config.get('builtin-server', False):
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://codereview.appspot.com/12086044/diff/4001/hooks/backend.py#newcode143
hooks/backend.py:143: utils.JUJU_GUI_DIR, utils.get_api_address(),
utils.J...

Read more...

Revision history for this message
Nicola Larosa (teknico) wrote :
Download full text (5.2 KiB)

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://codereview.appspot.com/12086044/diff/4001/Operation.md#newcode40
> 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://codereview.appspot.com/12086044/diff/4001/config.yaml
> File config.yaml (right):

https://codereview.appspot.com/12086044/diff/4001/config.yaml#newcode171
> 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://codereview.appspot.com/12086044/diff/4001/hooks/backend.py
> File hooks/backend.py (right):

https://codereview.appspot.com/12086044/diff/4001/hooks/backend.py#newcode24
> hooks/backend.py:24:
> 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://codereview.appspot.com/12086044/diff/4001/hooks/backend.py#newcode139
> hooks/backend.py:139: # TODO: eventually the option, haproxy and
> Apache will go away
> a bit confusing.

> eventually this option will go away, as well as haproxy and Apache

> or similar?

Done.

https://codereview.appspot.com/12086044/diff/4001/hooks/backend.py#newcode141
> hooks/backend.py:141: if config.get('builtin-server', False):
> For the change story, do you think we ought to remove the old
> hap...

Read more...

Revision history for this message
Nicola Larosa (teknico) wrote :
Download full text (5.3 KiB)

frankban wrote:

https://codereview.appspot.com/12086044/diff/4001/config/guiserver.conf.template#newcode7
> config/guiserver.conf.template:7: exec runserver.py
--guiroot="{{gui_root}}" \
> We could use the full path to runserver.py here.

To avoid possible interference with namesakes earlier in PATH. Done.

https://codereview.appspot.com/12086044/diff/4001/config/guiserver.conf.template#newcode9
> config/guiserver.conf.template:9: {{if api_version}}
> 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://codereview.appspot.com/12086044/diff/4001/hooks/backend.py#newcode24
> hooks/backend.py:24:
> 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://codereview.appspot.com/12086044/diff/4001/hooks/backend.py#newcode35
> hooks/backend.py:35: import charmhelpers
> Thank you for reordering imports.

One of those little things. :-)

https://codereview.appspot.com/12086044/diff/4001/hooks/backend.py#newcode140
> hooks/backend.py:140: api_version = 'python' if utils.legacy_juju()
> else 'go'
> I think the definition of api_version can be moved inside the "if
> config.get('builtin-server', False)" block below.

Done.

> Anyway, is there a reason to prefer config.get('builtin-server',
False) over
> config['builtin-server']?

Not really, as discussed I also removed other useless similar cases. The
only ones left are for SSL options without defaults.

https://codereview.appspot.com/12086044/diff/4001/hooks/backend.py#newcode141
> hooks/backend.py:141: if config.get('builtin-server', False):
> 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://codereview.appspot.com/12086044/diff/4001/hooks/backend.py#newcode143
> hooks/backend.py:143: utils.JUJU_GUI_DIR, utils.get_api_address(),
> 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_api_address() seems wrong too. Please take a...

Read more...

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.

Revision history for this message
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.

https://codereview.appspot.com/12086044/

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

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

Subscribers

People subscribed via source and target branches