Merge lp:~teknico/charms/precise/juju-gui/integrate-builtin-server-2 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-2
Merge into: lp:~juju-gui/charms/precise/juju-gui/trunk
Diff against target: 794 lines (+267/-90)
16 files modified
Makefile (+1/-1)
Operation.md (+13/-2)
config.yaml (+7/-0)
config/apache-site.template (+1/-1)
config/guiserver.conf.template (+13/-0)
hooks/backend.py (+28/-1)
hooks/utils.py (+90/-32)
revision (+1/-1)
server/guiserver/apps.py (+2/-2)
server/guiserver/manage.py (+3/-2)
server/guiserver/tests/test_apps.py (+2/-2)
server/runserver.py (+2/-2)
server/setup.py (+1/-1)
tests/20-functional.test (+17/-3)
tests/test_backends.py (+23/-15)
tests/test_utils.py (+63/-25)
To merge this branch: bzr merge lp:~teknico/charms/precise/juju-gui/integrate-builtin-server-2
Reviewer Review Type Date Requested Status
charmers Pending
Review via email: mp+179138@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 .

Add tests for the new functions in hooks/utils.py .

Clean up all paths and constants in hooks/utils.py .

Miscellaneuos cleanup.

QA1 (quick):

$ make unittest

QA2 (long, 40 minutes):

$ make ftest

QA3 (long but shorter than the former):

$ juju bootstrap
$ make deploy
  (check web access)
$ juju set juju-gui builtin-server=true
  (check web access)
$ juju set juju-gui builtin-server=false
  (check web access)
$ juju destroy-environment

https://codereview.appspot.com/12650044/

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

Reviewers: mp+179138_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 .

Add tests for the new functions in hooks/utils.py .

Clean up all paths and constants in hooks/utils.py .

Miscellaneuos cleanup.

QA1 (quick):

$ make unittest

QA2 (long, 40 minutes):

$ make ftest

QA3 (long but shorter than the former):

$ juju bootstrap
$ make deploy
   (check web access)
$ juju set juju-gui builtin-server=true
   (check web access)
$ juju set juju-gui builtin-server=false
   (check web access)
$ juju destroy-environment

https://code.launchpad.net/~teknico/charms/precise/juju-gui/integrate-builtin-server-2/+merge/179138

(do not edit description out of merge proposal)

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

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 revision
   M server/setup.py
   M tests/20-functional.test
   M tests/test_backends.py
   M tests/test_utils.py

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

This branch is nice Nicola, we are almost there, thank you.
Please fix the problem I described below, I will QA again later.

https://codereview.appspot.com/12650044/diff/1/config/guiserver.conf.template
File config/guiserver.conf.template (right):

https://codereview.appspot.com/12650044/diff/1/config/guiserver.conf.template#newcode7
config/guiserver.conf.template:7: exec /usr/bin/python
/usr/local/bin/runserver.py \
Cool.

https://codereview.appspot.com/12650044/diff/1/hooks/utils.py
File hooks/utils.py (right):

https://codereview.appspot.com/12650044/diff/1/hooks/utils.py#newcode110
hooks/utils.py:110: SERVER_DIR = os.path.join(CURRENT_DIR, 'server')
I am not sure I like all those constants. I agree they are used in more
than one place (but usually only two times AFAIK, and e.g. UNIT_NAME is
used only once), so this prevents typos, but I also liked the
possibility to read the functions without having to jump at the
beginning of the file. As I said, I see the value in this change, and I
can live with this.
The real request here is to fix all those "os.path.join('', ...": see
below.

https://codereview.appspot.com/12650044/diff/1/hooks/utils.py#newcode132
hooks/utils.py:132: config_json = Serializer(os.path.join('', 'tmp',
'config.json'))
I don't think this is the same as before: this results in
"tmp/config.json".
You might want to use "os.path.join(os.path.sep, 'tmp', 'config.json')",
but I am also ok with the original version.
Note that this generates an install error, that prevented me to QA this
branch. I'll retry it later when this is fixed.

https://codereview.appspot.com/12650044/diff/1/hooks/utils.py#newcode549
hooks/utils.py:549: api_url = '{}://127.0.0.1:{}'.format(
The "x if y else z" is not so easy to read when it is not a one-liner
IMHO.
I had to read this carefully, perhaps the more usual "if: else:" can be
better?

https://codereview.appspot.com/12650044/diff/1/hooks/utils.py#newcode564
hooks/utils.py:564: def start_builtin_server(build_dir, serve_tests,
ssl_cert_path, insecure):
What do you think about inverting serve_tests and ssl_cert_path? This
way we can have a similar signature for start_builtin_server and
write_builtin_server_startup.

https://codereview.appspot.com/12650044/diff/1/tests/20-functional.test
File tests/20-functional.test (right):

https://codereview.appspot.com/12650044/diff/1/tests/20-functional.test#newcode189
tests/20-functional.test:189: self.assertIn('TornadoServer',
server_header)
The header check is great. I'd also add an environment check, i.e. we
must ensure that the WebSocket is connected (basically the same thing
we do in the other functional tests).

https://codereview.appspot.com/12650044/

Revision history for this message
Nicola Larosa (teknico) wrote :

frankban wrote:

https://codereview.appspot.com/12650044/diff/1/hooks/utils.py#newcode110
> hooks/utils.py:110: SERVER_DIR = os.path.join(CURRENT_DIR, 'server')
> I also liked the possibility to read the functions without having
> to jump at the beginning of the file.

Hopefully the names of the constants are meaningful enough that you
don't need to see the associated values to be able to understand the
code.

> As I said, I see the value in this change, and I can live with this.

Good.

https://codereview.appspot.com/12650044/diff/1/hooks/utils.py#newcode132
> hooks/utils.py:132: config_json = Serializer(os.path.join('', 'tmp',
> 'config.json'))
> I don't think this is the same as before: this results in
> "tmp/config.json". You might want to use "os.path.join(os.path.sep,
> 'tmp', 'config.json')", but I am also ok with the original version.

Oh, sorry, I did not check it, will fix.

> Note that this generates an install error, that prevented me to
> QA this branch. I'll retry it later when this is fixed.

Weird, this did not happen to me in QA.

https://codereview.appspot.com/12650044/diff/1/hooks/utils.py#newcode549
> hooks/utils.py:549: api_url = '{}://127.0.0.1:{}'.format(
> The "x if y else z" is not so easy to read when it is not a
> one-liner IMHO. I had to read this carefully, perhaps the more
> usual "if: else:" can be better?

Hrmpf. You know I like it, and I know you don't like it. Is it really
that ugly? :-)

https://codereview.appspot.com/12650044/diff/1/hooks/utils.py#newcode564
> hooks/utils.py:564: def start_builtin_server(build_dir, serve_tests,
> ssl_cert_path, insecure):
> What do you think about inverting serve_tests and ssl_cert_path?
> This way we can have a similar signature for start_builtin_server
> and write_builtin_server_startup.

Uhm, I don't like it either but there was a reason for it, I'll check.

https://codereview.appspot.com/12650044/diff/1/tests/20-functional.test#newcode189
> tests/20-functional.test:189: self.assertIn('TornadoServer',
server_header)
> The header check is great. I'd also add an environment check, i.e. we
> must ensure that the WebSocket is connected (basically the same thing
> we do in the other functional tests).

Will do.

https://codereview.appspot.com/12650044/

89. By Nicola Larosa

Fixes per review.

90. By Nicola Larosa

More fixes.

91. By Nicola Larosa

Merge from trunk.

Revision history for this message
Nicola Larosa (teknico) wrote :
Revision history for this message
Madison Scott-Clary (makyo) wrote :

Code looks good with Francesco's comments, QAing now.

https://codereview.appspot.com/12650044/diff/1/hooks/utils.py
File hooks/utils.py (right):

https://codereview.appspot.com/12650044/diff/1/hooks/utils.py#newcode549
hooks/utils.py:549: api_url = '{}://127.0.0.1:{}'.format(
On 2013/08/08 11:12:58, frankban wrote:
> The "x if y else z" is not so easy to read when it is not a one-liner
IMHO.
> I had to read this carefully, perhaps the more usual "if: else:" can
be better?

I agree in this instance. The x if y else z construct works much better
in a single-line format than across multiple lines; in this instance,
it's hard to tell that this is conditional until halfway through, then
you have to parse it inside out across multiple lines.

https://codereview.appspot.com/12650044/

Revision history for this message
Madison Scott-Clary (makyo) wrote :
Revision history for this message
Francesco Banconi (frankban) wrote :

LGTM with the changes below.

I run the functional tests, they were passing, the last one was stopped
by the timeout: 40 mins are not enough, please increase that value (60
mins?).

QA:
switching from the legacy to the new server (and back) worked well. The
builtin server stopped working after setting secure=false on the charm.
I think upstart is not happy with the generated configuration file when
--insecure is passed.

I believe this branch can land with the timeout fix, but please add a
card to fix the "insecure" bug. Thank you!

https://codereview.appspot.com/12650044/

Revision history for this message
Nicola Larosa (teknico) wrote :

frankban wrote:
> I run the functional tests, they were passing, the last one was
stopped
> by the timeout: 40 mins are not enough, please increase that value
> (60 mins?).

Timeout increased to 60 minutes.

> QA:
> switching from the legacy to the new server (and back) worked well.
The
> I think upstart builtin server stopped working after setting
> secure=false on the charm. is not happy with the generated
> configuration file when --insecure is passed.

Indeed, there was a mistake in config/guiserver.conf.template. Fixed,
thank you.

https://codereview.appspot.com/12650044/

92. By Nicola Larosa

Increase functional tests timeout, fix config/guiserver.conf.template .

93. By Nicola Larosa

Fix the insecure and testsroot options handling of the server.

94. By Nicola Larosa

Remove leftover assert in test.

95. By Nicola Larosa

Better invocation example, shorten conditions.

Revision history for this message
Nicola Larosa (teknico) wrote :

*** Submitted:

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 .

Add tests for the new functions in hooks/utils.py .

Clean up all paths and constants in hooks/utils.py .

Miscellaneuos cleanup.

QA1 (quick):

$ make unittest

QA2 (long, 40 minutes):

$ make ftest

QA3 (long but shorter than the former):

$ juju bootstrap
$ make deploy
   (check web access)
$ juju set juju-gui builtin-server=true
   (check web access)
$ juju set juju-gui builtin-server=false
   (check web access)
$ juju destroy-environment

R=frankban, matthew.scott
CC=
https://codereview.appspot.com/12650044

https://codereview.appspot.com/12650044/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'Makefile'
2--- Makefile 2013-08-06 14:17:23 +0000
3+++ Makefile 2013-08-09 13:41:32 +0000
4@@ -14,7 +14,7 @@
5 # You should have received a copy of the GNU Affero General Public License
6 # along with this program. If not, see <http://www.gnu.org/licenses/>.
7
8-JUJUTEST = juju-test --timeout=40m -v -e "$(JUJU_ENV)" --upload-tools
9+JUJUTEST = juju-test --timeout=60m -v -e "$(JUJU_ENV)" --upload-tools
10 VENV = ./tests/.venv
11
12 all: setup
13
14=== modified file 'Operation.md'
15--- Operation.md 2013-07-29 10:40:50 +0000
16+++ Operation.md 2013-08-09 13:41:32 +0000
17@@ -12,8 +12,8 @@
18 ## How it works ##
19
20 The Juju GUI is a client-side, JavaScript application that runs inside a
21-web browser. The browser connects to a custom-made server deployed by
22-the charm.
23+web browser. The browser connects to a built-in server deployed by the
24+charm.
25
26 ## Server ##
27
28@@ -27,3 +27,14 @@
29 Juju connections are bidirectional, using the WebSocket protocol on the
30 same port as the HTTPS connection, allowing changes in the Juju
31 environment to be propagated and shown immediately by the browser.
32+
33+## Activation ##
34+
35+Previously the Juju GUI has been served by a combination of haproxy and
36+Apache, specifically deployed and configured by the charm.
37+
38+The new built-in server replaces them both and can be enabled by
39+setting the config option `builtin-server` to `true`.
40+
41+In the future haproxy, Apache and the mentioned config option will be
42+removed; only the built-in server will remain.
43
44=== modified file 'config.yaml'
45--- config.yaml 2013-08-05 09:38:39 +0000
46+++ config.yaml 2013-08-09 13:41:32 +0000
47@@ -166,3 +166,10 @@
48 a link to juju.ubuntu.com
49 type: boolean
50 default: false
51+ builtin-server:
52+ description: |
53+ Enable the built-in server, disabling both haproxy and Apache.
54+ This is a temporary option: the built-in server will be
55+ the only server in the future.
56+ type: boolean
57+ default: false
58
59=== modified file 'config/apache-site.template'
60--- config/apache-site.template 2013-08-01 14:33:00 +0000
61+++ config/apache-site.template 2013-08-09 13:41:32 +0000
62@@ -21,7 +21,7 @@
63
64 CustomLog ${APACHE_LOG_DIR}/access.log combined
65
66- {{if serve_tests}}
67+ {{if tests_root}}
68 Alias /test {{tests_root}}
69 {{endif}}
70
71
72=== added file 'config/guiserver.conf.template'
73--- config/guiserver.conf.template 1970-01-01 00:00:00 +0000
74+++ config/guiserver.conf.template 2013-08-09 13:41:32 +0000
75@@ -0,0 +1,13 @@
76+description "GUIServer"
77+author "Canonical"
78+
79+start on (filesystem and net-device-up IFACE=lo)
80+stop on runlevel [!2345]
81+
82+exec /usr/bin/python /usr/local/bin/runserver.py \
83+ --guiroot="{{gui_root}}" \
84+ --apiurl="{{api_url}}" \
85+ --apiversion="{{api_version}}" \
86+ --sslpath="{{ssl_cert_path}}" \
87+ {{if tests_root}} --testsroot="{{tests_root}}" {{endif}} \
88+ {{if insecure}} --insecure {{endif}}
89
90=== added directory 'deps'
91=== added file 'deps/tornado-3.1.tar.gz'
92Binary files deps/tornado-3.1.tar.gz 1970-01-01 00:00:00 +0000 and deps/tornado-3.1.tar.gz 2013-08-09 13:41:32 +0000 differ
93=== modified file 'hooks/backend.py'
94--- hooks/backend.py 2013-08-05 09:59:17 +0000
95+++ hooks/backend.py 2013-08-09 13:41:32 +0000
96@@ -180,6 +180,28 @@
97 utils.stop_haproxy_apache()
98
99
100+class BuiltinServerMixin(ServerInstallMixinBase):
101+ """Manage the builtin server via Upstart."""
102+
103+ debs = ('openssl', 'python-pip')
104+
105+ def install(self, backend):
106+ utils.install_tornado()
107+ utils.install_builtin_server()
108+ self._setup_certificates(backend)
109+
110+ def start(self, backend):
111+ config = backend.config
112+ build_dir = utils.compute_build_dir(
113+ config['staging'], config['serve-tests'])
114+ utils.start_builtin_server(
115+ build_dir, config['ssl-cert-path'], config['serve-tests'],
116+ not config['secure'])
117+
118+ def stop(self, backend):
119+ utils.stop_builtin_server()
120+
121+
122 def chain_methods(name):
123 """Helper to compose a set of mixin objects into a callable.
124
125@@ -249,7 +271,12 @@
126
127 # We always install and start the GUI.
128 self.mixins.append(GuiMixin())
129- self.mixins.append(HaproxyApacheMixin())
130+ # TODO: eventually this option will go away, as well as haproxy and
131+ # Apache.
132+ if config['builtin-server']:
133+ self.mixins.append(BuiltinServerMixin())
134+ else:
135+ self.mixins.append(HaproxyApacheMixin())
136
137 def different(self, *keys):
138 """Return a boolean indicating if the current config
139
140=== modified file 'hooks/utils.py'
141--- hooks/utils.py 2013-08-08 08:13:24 +0000
142+++ hooks/utils.py 2013-08-09 13:41:32 +0000
143@@ -17,19 +17,14 @@
144 """Juju GUI charm utilities."""
145
146 __all__ = [
147- 'AGENT',
148- 'APACHE',
149 'APACHE_SITE',
150 'APACHE_PORTS',
151 'API_PORT',
152 'CURRENT_DIR',
153- 'HAPROXY',
154- 'IMPROV',
155 'JUJU_DIR',
156 'JUJU_GUI_DIR',
157 'JUJU_PEM',
158 'WEB_PORT',
159- 'bzr_checkout',
160 'cmd_log',
161 'compute_build_dir',
162 'fetch_api',
163@@ -53,9 +48,11 @@
164 'setup_gui',
165 'setup_haproxy_config',
166 'start_agent',
167+ 'start_builtin_server',
168 'start_haproxy_apache',
169 'start_improv',
170 'stop_agent',
171+ 'stop_builtin_server',
172 'stop_haproxy_apache',
173 'stop_improv',
174 'write_gui_config',
175@@ -99,20 +96,31 @@
176
177 AGENT = 'juju-api-agent'
178 APACHE = 'apache2'
179+BUILTIN_SERVER = 'guiserver'
180+HAPROXY = 'haproxy'
181 IMPROV = 'juju-api-improv'
182-HAPROXY = 'haproxy'
183
184 API_PORT = 8080
185 WEB_PORT = 8000
186
187 CURRENT_DIR = os.getcwd()
188+CONFIG_DIR = os.path.join(CURRENT_DIR, 'config')
189 JUJU_DIR = os.path.join(CURRENT_DIR, 'juju')
190 JUJU_GUI_DIR = os.path.join(CURRENT_DIR, 'juju-gui')
191-APACHE_SITE = '/etc/apache2/sites-available/juju-gui'
192-APACHE_PORTS = '/etc/apache2/ports.conf'
193-HAPROXY_PATH = '/etc/haproxy/haproxy.cfg'
194-SYS_INIT_DIR = '/etc/init/'
195-CONFIG_DIR = os.path.join(os.path.dirname(__file__), '..', 'config')
196+SERVER_DIR = os.path.join(CURRENT_DIR, 'server')
197+TORNADO_PATH = os.path.join(CURRENT_DIR, 'deps', 'tornado-3.1.tar.gz')
198+
199+APACHE_CFG_DIR = os.path.join(os.path.sep, 'etc', 'apache2')
200+APACHE_PORTS = os.path.join(APACHE_CFG_DIR, 'ports.conf')
201+APACHE_SITE = os.path.join(APACHE_CFG_DIR, 'sites-available', 'juju-gui')
202+HAPROXY_CFG_PATH = os.path.join(os.path.sep, 'etc', 'haproxy', 'haproxy.cfg')
203+
204+SYS_INIT_DIR = os.path.join(os.path.sep, 'etc', 'init')
205+AGENT_INIT_PATH = os.path.join(SYS_INIT_DIR, 'juju-api-agent.conf')
206+GUISERVER_INIT_PATH = os.path.join(SYS_INIT_DIR, 'guiserver.conf')
207+HAPROXY_INIT_PATH = os.path.join(SYS_INIT_DIR, 'haproxy.conf')
208+IMPROV_INIT_PATH = os.path.join(SYS_INIT_DIR, 'juju-api-improv.conf')
209+
210 JUJU_PEM = 'juju.includes-private-key.pem'
211 DEB_BUILD_DEPENDENCIES = (
212 'bzr', 'g++', 'imagemagick', 'make', 'nodejs', 'npm',
213@@ -120,7 +128,7 @@
214
215
216 # Store the configuration from on invocation to the next.
217-config_json = Serializer('/tmp/config.json')
218+config_json = Serializer(os.path.join(os.path.sep, 'tmp', 'config.json'))
219 # Bazaar checkout command.
220 bzr_checkout = command('bzr', 'co', '--lightweight')
221 # Whether or not the charm is deployed using juju-core.
222@@ -289,8 +297,7 @@
223 The argument *destination* is a file path.
224 The argument *context* is a dict-like object.
225 """
226- template_path = os.path.join(
227- os.path.dirname(__file__), '..', 'config', template_name)
228+ template_path = os.path.join(CONFIG_DIR, template_name)
229 template = tempita.Template.from_filename(template_path)
230 with open(destination, 'w') as stream:
231 stream.write(template.substitute(context))
232@@ -328,9 +335,7 @@
233 'port': API_PORT,
234 'staging_env': staging_env,
235 }
236- render_to_file(
237- 'juju-api-improv.conf.template', context,
238- '/etc/init/juju-api-improv.conf')
239+ render_to_file('juju-api-improv.conf.template', context, IMPROV_INIT_PATH)
240 log('Starting the staging backend.')
241 with su('root'):
242 service_control(IMPROV, START)
243@@ -342,15 +347,15 @@
244 with su('root'):
245 service_control(IMPROV, STOP)
246 log('Removing the staging Upstart script.')
247- cmd_log(run('rm', '-f', '/etc/init/juju-api-improv.conf'))
248+ cmd_log(run('rm', '-f', IMPROV_INIT_PATH))
249
250
251 def start_agent(ssl_cert_path, read_only=False):
252 """Start the Juju agent and connect to the current environment."""
253 # Retrieve the Zookeeper address from the start up script.
254- unit_dir = os.path.realpath(os.path.join(CURRENT_DIR, '..'))
255- agent_file = os.path.join(
256- SYS_INIT_DIR, 'juju-{}.conf'.format(os.path.basename(unit_dir)))
257+ unit_name = os.path.basename(
258+ os.path.realpath(os.path.join(CURRENT_DIR, '..')))
259+ agent_file = os.path.join(SYS_INIT_DIR, 'juju-{}.conf'.format(unit_name))
260 zookeeper = get_zookeeper_address(agent_file)
261 log('Setting up the API agent Upstart script.')
262 context = {
263@@ -360,9 +365,7 @@
264 'zookeeper': zookeeper,
265 'read_only': read_only
266 }
267- render_to_file(
268- 'juju-api-agent.conf.template', context,
269- '/etc/init/juju-api-agent.conf')
270+ render_to_file('juju-api-agent.conf.template', context, AGENT_INIT_PATH)
271 log('Starting the API agent.')
272 with su('root'):
273 service_control(AGENT, START)
274@@ -374,7 +377,7 @@
275 with su('root'):
276 service_control(AGENT, STOP)
277 log('Removing the API agent Upstart script.')
278- cmd_log(run('rm', '-f', '/etc/init/juju-api-agent.conf'))
279+ cmd_log(run('rm', '-f', AGENT_INIT_PATH))
280
281
282 def compute_build_dir(in_staging, serve_tests):
283@@ -442,7 +445,7 @@
284 is_legacy_juju = legacy_juju()
285 if is_legacy_juju:
286 # The PyJuju API agent is listening on localhost.
287- api_address = '127.0.0.1:{0}'.format(API_PORT)
288+ api_address = '127.0.0.1:{}'.format(API_PORT)
289 else:
290 # Retrieve the juju-core API server address.
291 api_address = get_api_address()
292@@ -458,25 +461,24 @@
293 'web_port': WEB_PORT,
294 'secure': secure
295 }
296- render_to_file('haproxy.cfg.template', context, HAPROXY_PATH)
297+ render_to_file('haproxy.cfg.template', context, HAPROXY_CFG_PATH)
298
299
300 def remove_haproxy_setup():
301 """Remove haproxy setup."""
302 log('Removing haproxy setup.')
303- cmd_log(run('rm', '-f', HAPROXY_PATH))
304- config_path = os.path.join(SYS_INIT_DIR, 'haproxy.conf')
305- cmd_log(run('rm', '-f', config_path))
306+ cmd_log(run('rm', '-f', HAPROXY_CFG_PATH))
307+ cmd_log(run('rm', '-f', HAPROXY_INIT_PATH))
308
309
310 def setup_apache_config(build_dir, serve_tests=False):
311 """Set up the Apache configuration."""
312 log('Generating the Apache site configuration files.')
313+ tests_root = os.path.join(JUJU_GUI_DIR, 'test', '') if serve_tests else ''
314 context = {
315 'port': WEB_PORT,
316- 'serve_tests': serve_tests,
317 'server_root': build_dir,
318- 'tests_root': os.path.join(JUJU_GUI_DIR, 'test', ''),
319+ 'tests_root': tests_root,
320 }
321 render_to_file('apache-ports.template', context, APACHE_PORTS)
322 cmd_log(run('chown', 'ubuntu:', APACHE_PORTS))
323@@ -523,6 +525,62 @@
324 remove_apache_setup()
325
326
327+def install_tornado():
328+ """Install Tornado from a local tarball."""
329+ log('Installing Tornado.')
330+ with su('root'):
331+ cmd_log(run('pip', 'install', TORNADO_PATH))
332+
333+
334+def install_builtin_server():
335+ """Install the builtin server code."""
336+ log('Installing the builtin server.')
337+ setup_cmd = os.path.join(SERVER_DIR, 'setup.py')
338+ with su('root'):
339+ cmd_log(run('/usr/bin/python', setup_cmd, 'install'))
340+
341+
342+def write_builtin_server_startup(
343+ gui_root, ssl_cert_path, serve_tests=False, insecure=False):
344+ """Generate the builtin server Upstart file."""
345+ log('Generating the builtin server Upstart file.')
346+ url_prefix = 'ws' if insecure else 'wss'
347+ is_legacy_juju = legacy_juju()
348+ api_address = get_api_address()
349+ if is_legacy_juju:
350+ api_url = '{}://127.0.0.1:{}'.format(url_prefix, API_PORT)
351+ else:
352+ api_url = '{}://{}'.format(url_prefix, api_address)
353+ tests_root = os.path.join(JUJU_GUI_DIR, 'test', '') if serve_tests else ''
354+ context = {
355+ 'gui_root': gui_root,
356+ 'api_url': api_url,
357+ 'api_version': 'python' if is_legacy_juju else 'go',
358+ 'ssl_cert_path': ssl_cert_path,
359+ 'tests_root': tests_root,
360+ 'insecure': insecure,
361+ }
362+ render_to_file(
363+ 'guiserver.conf.template', context, GUISERVER_INIT_PATH)
364+
365+
366+def start_builtin_server(build_dir, ssl_cert_path, serve_tests, insecure):
367+ """Start the builtin server."""
368+ write_builtin_server_startup(
369+ build_dir, ssl_cert_path, serve_tests, insecure)
370+ log('Starting the builtin server.')
371+ with su('root'):
372+ service_control(BUILTIN_SERVER, RESTART)
373+
374+
375+def stop_builtin_server():
376+ """Stop the builtin server."""
377+ log('Stopping the builtin server.')
378+ with su('root'):
379+ service_control(BUILTIN_SERVER, STOP)
380+ cmd_log(run('rm', '-f', GUISERVER_INIT_PATH))
381+
382+
383 def get_npm_cache_archive_url(Launchpad=Launchpad):
384 """Figure out the URL of the most recent NPM cache archive on Launchpad."""
385 launchpad = Launchpad.login_anonymously('Juju GUI charm', 'production')
386
387=== modified file 'revision'
388--- revision 2013-08-08 13:09:10 +0000
389+++ revision 2013-08-09 13:41:32 +0000
390@@ -1,1 +1,1 @@
391-67
392+68
393
394=== modified file 'server/guiserver/apps.py'
395--- server/guiserver/apps.py 2013-08-07 16:00:27 +0000
396+++ server/guiserver/apps.py 2013-08-09 13:41:32 +0000
397@@ -50,8 +50,8 @@
398 (r'^/juju-ui/(.*)', web.StaticFileHandler, {'path': static_path}),
399 (r'^/(favicon\.ico)$', web.StaticFileHandler, {'path': guiroot}),
400 ]
401- if options.servetests:
402- params = {'path': options.servetests, 'default_filename': 'index.html'}
403+ if options.testsroot:
404+ params = {'path': options.testsroot, 'default_filename': 'index.html'}
405 server_handlers.append(
406 # Serve the Juju GUI tests.
407 (r'^/test/(.*)', web.StaticFileHandler, params),
408
409=== modified file 'server/guiserver/manage.py'
410--- server/guiserver/manage.py 2013-08-05 09:38:39 +0000
411+++ server/guiserver/manage.py 2013-08-09 13:41:32 +0000
412@@ -100,8 +100,9 @@
413 help='the Juju API version/implementation. Currently the possible '
414 'values are "go" (default) or "python".')
415 define(
416- 'servetests', type=str,
417- help='The Juju GUI tests path. If not provided, tests are not served.')
418+ 'testsroot', type=str,
419+ help='The filesystem path of the Juju GUI tests directory. '
420+ 'If not provided, tests are not served.')
421 define(
422 'sslpath', type=str, default=DEFAULT_SSL_PATH,
423 help='The path where the SSL certificates are stored.')
424
425=== modified file 'server/guiserver/tests/test_apps.py'
426--- server/guiserver/tests/test_apps.py 2013-08-07 16:36:38 +0000
427+++ server/guiserver/tests/test_apps.py 2013-08-09 13:41:32 +0000
428@@ -77,7 +77,7 @@
429
430 def test_serving_gui_tests(self):
431 # The server can be configured to serve GUI unit tests.
432- app = self.get_app(servetests='/my/tests/')
433+ app = self.get_app(testsroot='/my/tests/')
434 spec = self.get_url_spec(app, r'^/test/(.*)$')
435 self.assertIsNotNone(spec)
436 self.assertIn('path', spec.kwargs)
437@@ -85,7 +85,7 @@
438
439 def test_not_serving_gui_tests(self):
440 # The server can be configured to avoid serving GUI unit tests.
441- app = self.get_app(servetests=None)
442+ app = self.get_app(testsroot=None)
443 spec = self.get_url_spec(app, r'^/test/(.*)$')
444 self.assertIsNone(spec)
445
446
447=== modified file 'server/runserver.py'
448--- server/runserver.py 2013-08-07 09:50:50 +0000
449+++ server/runserver.py 2013-08-09 13:41:32 +0000
450@@ -20,8 +20,8 @@
451 --guiroot="/var/lib/juju/agents/unit-juju-gui-0/charm/juju-gui/build-prod"
452 --apiurl="wss://ec2-75-101-177-185.compute-1.example.com:17070"
453 --apiversion="go"
454- --sslpath=/etc/ssl/juju-gui
455- --servetests
456+ --sslpath="/etc/ssl/juju-gui"
457+ --tests_root="/var/lib/juju/agents/unit-juju-gui-0/charm/juju-gui/test/"
458 --insecure
459
460 The --sslpath option is ignored if --insecure is set.
461
462=== modified file 'server/setup.py'
463--- server/setup.py 2013-07-17 15:20:47 +0000
464+++ server/setup.py 2013-08-09 13:41:32 +0000
465@@ -38,7 +38,7 @@
466 keywords='juju gui server',
467 packages=[
468 PROJECT_NAME,
469- '{0}.tests'.format(PROJECT_NAME),
470+ '{}.tests'.format(PROJECT_NAME),
471 ],
472 scripts=['runserver.py'],
473 classifiers=[
474
475=== modified file 'tests/20-functional.test'
476--- tests/20-functional.test 2013-08-05 15:00:27 +0000
477+++ tests/20-functional.test 2013-08-09 13:41:32 +0000
478@@ -82,7 +82,7 @@
479 Retry loading the page until the page is found or a timeout exception
480 is raised.
481 """
482- base_url = 'https://{0}:{1}'.format(hostname, self.port)
483+ base_url = 'https://{}:{}'.format(hostname, self.port)
484 url = urlparse.urljoin(base_url, path)
485
486 def page_ready(driver):
487@@ -134,7 +134,7 @@
488 # started by the charm in the machine. Right now this does not
489 # work in pyJuju, so the desired effect is achieved by keeping
490 # track of started services and manually stopping them here.
491- target = 'ubuntu@{0}'.format(hostname)
492+ target = 'ubuntu@{}'.format(hostname)
493 for service in services:
494 ssh(target, 'sudo', 'service', service, 'stop')
495
496@@ -177,6 +177,20 @@
497 # The staging environment contains five deployed services.
498 self.assertSetEqual(set(STAGING_SERVICES), self.get_service_names())
499
500+ def test_builtin_server(self):
501+ # Ensure the Juju GUI and builtin server are correctly set up.
502+ unit_info = self.juju_deploy(
503+ self.charm, options={'builtin-server': 'true'})
504+ hostname = unit_info['public-address']
505+ self.navigate_to(hostname)
506+ self.handle_browser_warning()
507+ self.assertEnvironmentIsConnected()
508+ conn = httplib.HTTPSConnection(hostname)
509+ conn.request('HEAD', '/')
510+ headers = conn.getresponse().getheaders()
511+ server_header = dict(headers)['server']
512+ self.assertIn('TornadoServer', server_header)
513+
514 def test_branch_source(self):
515 # Ensure the Juju GUI is correctly deployed from a Bazaar branch.
516 unit_info = self.juju_deploy(
517@@ -192,7 +206,7 @@
518 self.charm, options={'juju-gui-source': JUJU_GUI_TEST_BRANCH})
519 hostname = unit_info['public-address']
520 conn = httplib.HTTPSConnection(hostname)
521- conn.request('GET', '/')
522+ conn.request('HEAD', '/')
523 headers = conn.getresponse().getheaders()
524 # There is only one Cache-Control header.
525 self.assertEqual(zip(*headers)[0].count('cache-control'), 1)
526
527=== modified file 'tests/test_backends.py'
528--- tests/test_backends.py 2013-08-05 09:59:17 +0000
529+++ tests/test_backends.py 2013-08-09 13:41:32 +0000
530@@ -46,8 +46,8 @@
531 """Ensure the correct mixins and property values are collected."""
532
533 def test_staging_backend(self):
534- test_backend = backend.Backend(
535- config={'sandbox': False, 'staging': True})
536+ test_backend = backend.Backend(config={
537+ 'sandbox': False, 'staging': True, 'builtin-server': False})
538 mixin_names = get_mixin_names(test_backend)
539 self.assertEqual(
540 ('ImprovMixin', 'GuiMixin', 'HaproxyApacheMixin'),
541@@ -57,8 +57,8 @@
542 test_backend.debs)
543
544 def test_sandbox_backend(self):
545- test_backend = backend.Backend(
546- config={'sandbox': True, 'staging': False})
547+ test_backend = backend.Backend(config={
548+ 'sandbox': True, 'staging': False, 'builtin-server': False})
549 mixin_names = get_mixin_names(test_backend)
550 self.assertEqual(
551 ('SandboxMixin', 'GuiMixin', 'HaproxyApacheMixin'),
552@@ -68,8 +68,8 @@
553 test_backend.debs)
554
555 def test_python_backend(self):
556- test_backend = backend.Backend(
557- config={'sandbox': False, 'staging': False})
558+ test_backend = backend.Backend(config={
559+ 'sandbox': False, 'staging': False, 'builtin-server': False})
560 mixin_names = get_mixin_names(test_backend)
561 self.assertEqual(
562 ('PythonMixin', 'GuiMixin', 'HaproxyApacheMixin'),
563@@ -86,8 +86,8 @@
564 # Create a fake agent file.
565 agent_path = os.path.join(base_dir, 'agent.conf')
566 open(agent_path, 'w').close()
567- test_backend = backend.Backend(
568- config={'sandbox': False, 'staging': False})
569+ test_backend = backend.Backend(config={
570+ 'sandbox': False, 'staging': False, 'builtin-server': False})
571 # Cleanup.
572 utils.CURRENT_DIR = orig_current_dir
573 shutil.rmtree(base_dir)
574@@ -118,6 +118,8 @@
575 'find_missing_packages': utils.find_missing_packages,
576 'get_api_address': utils.get_api_address,
577 'get_npm_cache_archive_url': utils.get_npm_cache_archive_url,
578+ 'install_builtin_server': utils.install_builtin_server,
579+ 'install_tornado': utils.install_tornado,
580 'parse_source': utils.parse_source,
581 'prime_npm_cache': utils.prime_npm_cache,
582 'remove_apache_setup': utils.remove_apache_setup,
583@@ -128,6 +130,7 @@
584 'setup_haproxy_config': utils.setup_haproxy_config,
585 'start_agent': utils.start_agent,
586 'start_improv': utils.start_improv,
587+ 'write_builtin_server_startup': utils.write_builtin_server_startup,
588 'write_gui_config': utils.write_gui_config,
589 }
590 self.charmhelpers_mocks = {
591@@ -192,11 +195,12 @@
592 self.assertTrue(
593 self.called.get(mocked), '{} was not called'.format(mocked))
594
595- def test_install_improv(self):
596+ def test_install_improv_builtin(self):
597 test_backend = backend.Backend(config=self.alwaysTrue)
598 test_backend.install()
599 for mocked in (
600 'apt_get_install', 'fetch_api', 'find_missing_packages',
601+ 'install_builtin_server', 'install_tornado',
602 ):
603 self.assertTrue(
604 self.called.get(mocked), '{} was not called'.format(mocked))
605@@ -211,12 +215,12 @@
606 self.assertTrue(
607 self.called.get(mocked), '{} was not called'.format(mocked))
608
609- def test_start_improv(self):
610+ def test_start_improv_builtin(self):
611 test_backend = backend.Backend(config=self.alwaysTrue)
612 test_backend.start()
613 for mocked in (
614 'compute_build_dir', 'open_port', 'start_improv', 'su',
615- 'write_gui_config',
616+ 'write_builtin_server_startup', 'write_gui_config',
617 ):
618 self.assertTrue(
619 self.called.get(mocked), '{} was not called'.format(mocked))
620@@ -231,16 +235,20 @@
621
622 def test_same_config(self):
623 test_backend = backend.Backend(
624- config={'sandbox': False, 'staging': False},
625- prev_config={'sandbox': False, 'staging': False},
626+ config={
627+ 'sandbox': False, 'staging': False, 'builtin-server': False},
628+ prev_config={
629+ 'sandbox': False, 'staging': False, 'builtin-server': False},
630 )
631 self.assertFalse(test_backend.different('sandbox'))
632 self.assertFalse(test_backend.different('staging'))
633
634 def test_different_config(self):
635 test_backend = backend.Backend(
636- config={'sandbox': False, 'staging': False},
637- prev_config={'sandbox': True, 'staging': False},
638+ config={
639+ 'sandbox': False, 'staging': False, 'builtin-server': False},
640+ prev_config={
641+ 'sandbox': True, 'staging': False, 'builtin-server': False},
642 )
643 self.assertTrue(test_backend.different('sandbox'))
644 self.assertFalse(test_backend.different('staging'))
645
646=== modified file 'tests/test_utils.py'
647--- tests/test_utils.py 2013-08-07 09:50:50 +0000
648+++ tests/test_utils.py 2013-08-09 13:41:32 +0000
649@@ -45,6 +45,8 @@
650 log_hook,
651 parse_source,
652 get_npm_cache_archive_url,
653+ install_builtin_server,
654+ install_tornado,
655 remove_apache_setup,
656 remove_haproxy_setup,
657 render_to_file,
658@@ -52,11 +54,14 @@
659 setup_apache_config,
660 setup_haproxy_config,
661 start_agent,
662+ start_builtin_server,
663 start_haproxy_apache,
664 start_improv,
665 stop_agent,
666+ stop_builtin_server,
667 stop_haproxy_apache,
668 stop_improv,
669+ write_builtin_server_startup,
670 write_gui_config,
671 )
672 # Import the whole utils package for monkey patching.
673@@ -379,7 +384,7 @@
674
675 def setUp(self):
676 self.zookeeper_address = 'example.com:2000'
677- contents = 'env JUJU_ZOOKEEPER="{0}"\n'.format(self.zookeeper_address)
678+ contents = 'env JUJU_ZOOKEEPER="{}"\n'.format(self.zookeeper_address)
679 with tempfile.NamedTemporaryFile(delete=False) as agent_file:
680 agent_file.write(contents)
681 self.agent_file_path = agent_file.name
682@@ -674,30 +679,15 @@
683 result, build_dir, 'in_staging: {}, serve_tests: {}'.format(
684 in_staging, serve_tests))
685
686- def test_write_gui_config(self):
687- write_gui_config(
688- False, 'This is login help.', True, True, self.charmworld_url,
689- self.build_dir, use_analytics=True, config_js_path='config')
690- js_conf = self.files['config']
691- self.assertIn('consoleEnabled: false', js_conf)
692- self.assertIn('user: "admin"', js_conf)
693- self.assertIn('password: "admin"', js_conf)
694- self.assertIn('login_help: "This is login help."', js_conf)
695- self.assertIn('readOnly: true', js_conf)
696- self.assertIn("socket_url: 'wss://", js_conf)
697- self.assertIn('socket_protocol: "wss"', js_conf)
698- self.assertIn('charmworldURL: "http://charmworld.example"', js_conf)
699- self.assertIn('useAnalytics: true', js_conf)
700-
701 def test_setup_haproxy_config(self):
702 setup_haproxy_config(self.ssl_cert_path)
703 haproxy_conf = self.files['haproxy.cfg']
704- self.assertIn('ca-base {0}'.format(self.ssl_cert_path), haproxy_conf)
705- self.assertIn('crt-base {0}'.format(self.ssl_cert_path), haproxy_conf)
706- self.assertIn('ws1 127.0.0.1:{0}'.format(API_PORT), haproxy_conf)
707- self.assertIn('web1 127.0.0.1:{0}'.format(WEB_PORT), haproxy_conf)
708- self.assertIn('ca-file {0}'.format(JUJU_PEM), haproxy_conf)
709- self.assertIn('crt {0}'.format(JUJU_PEM), haproxy_conf)
710+ self.assertIn('ca-base {}'.format(self.ssl_cert_path), haproxy_conf)
711+ self.assertIn('crt-base {}'.format(self.ssl_cert_path), haproxy_conf)
712+ self.assertIn('ws1 127.0.0.1:{}'.format(API_PORT), haproxy_conf)
713+ self.assertIn('web1 127.0.0.1:{}'.format(WEB_PORT), haproxy_conf)
714+ self.assertIn('ca-file {}'.format(JUJU_PEM), haproxy_conf)
715+ self.assertIn('crt {}'.format(JUJU_PEM), haproxy_conf)
716 self.assertIn('redirect scheme https', haproxy_conf)
717
718 def test_remove_haproxy_setup(self):
719@@ -708,9 +698,9 @@
720 setup_apache_config(self.build_dir, serve_tests=True)
721 apache_site_conf = self.files['SITE_NOT_THERE']
722 self.assertIn('juju-gui/build-', apache_site_conf)
723- self.assertIn('VirtualHost *:{0}'.format(WEB_PORT), apache_site_conf)
724+ self.assertIn('VirtualHost *:{}'.format(WEB_PORT), apache_site_conf)
725 self.assertIn(
726- 'Alias /test {0}/test/'.format(JUJU_GUI_DIR), apache_site_conf)
727+ 'Alias /test {}/test/'.format(JUJU_GUI_DIR), apache_site_conf)
728 apache_ports_conf = self.files['PORTS_NOT_THERE']
729 self.assertIn('NameVirtualHost *:8000', apache_ports_conf)
730 self.assertIn('Listen 8000', apache_ports_conf)
731@@ -720,7 +710,7 @@
732 self.assertEqual(self.run_call_count, 3)
733
734 def test_start_haproxy_apache(self):
735- start_haproxy_apache('build_dir', False, self.ssl_cert_path, True)
736+ start_haproxy_apache(JUJU_GUI_DIR, False, self.ssl_cert_path, True)
737 self.assertEqual(self.svc_ctl_call_count, 2)
738 self.assertEqual(self.service_names, ['apache2', 'haproxy'])
739 self.assertEqual(
740@@ -732,6 +722,54 @@
741 self.assertEqual(self.service_names, ['haproxy', 'apache2'])
742 self.assertEqual(self.actions, [charmhelpers.STOP, charmhelpers.STOP])
743
744+ def test_install_tornado(self):
745+ install_tornado()
746+ self.assertEqual(self.run_call_count, 1)
747+
748+ def test_install_builtin_server(self):
749+ install_builtin_server()
750+ self.assertEqual(self.run_call_count, 1)
751+
752+ def test_write_builtin_server_startup(self):
753+ write_builtin_server_startup(
754+ JUJU_GUI_DIR, self.ssl_cert_path, serve_tests=True, insecure=True)
755+ guiserver_conf = self.files['guiserver.conf']
756+ self.assertIn('description "GUIServer"', guiserver_conf)
757+ self.assertIn('--apiurl="ws://127.0.0.1:8080"', guiserver_conf)
758+ self.assertIn('--apiversion="python"', guiserver_conf)
759+ self.assertIn(
760+ '--testsroot="{}/test/"'.format(JUJU_GUI_DIR), guiserver_conf)
761+ self.assertIn('--insecure', guiserver_conf)
762+
763+ def test_start_builtin_server(self):
764+ start_builtin_server(
765+ JUJU_GUI_DIR, self.ssl_cert_path, False, insecure=False)
766+ self.assertEqual(self.svc_ctl_call_count, 1)
767+ self.assertEqual(self.service_names, ['guiserver'])
768+ self.assertEqual(self.actions, [charmhelpers.RESTART])
769+
770+ def test_stop_builtin_server(self):
771+ stop_builtin_server()
772+ self.assertEqual(self.svc_ctl_call_count, 1)
773+ self.assertEqual(self.service_names, ['guiserver'])
774+ self.assertEqual(self.actions, [charmhelpers.STOP])
775+ self.assertEqual(self.run_call_count, 1)
776+
777+ def test_write_gui_config(self):
778+ write_gui_config(
779+ False, 'This is login help.', True, True, self.charmworld_url,
780+ self.build_dir, use_analytics=True, config_js_path='config')
781+ js_conf = self.files['config']
782+ self.assertIn('consoleEnabled: false', js_conf)
783+ self.assertIn('user: "admin"', js_conf)
784+ self.assertIn('password: "admin"', js_conf)
785+ self.assertIn('login_help: "This is login help."', js_conf)
786+ self.assertIn('readOnly: true', js_conf)
787+ self.assertIn("socket_url: 'wss://", js_conf)
788+ self.assertIn('socket_protocol: "wss"', js_conf)
789+ self.assertIn('charmworldURL: "http://charmworld.example"', js_conf)
790+ self.assertIn('useAnalytics: true', js_conf)
791+
792 def test_write_gui_config_insecure(self):
793 write_gui_config(
794 False, 'This is login help.', True, True, self.charmworld_url,

Subscribers

People subscribed via source and target branches