Merge lp:~benji/charms/precise/juju-gui/use-npm-cache into lp:~juju-gui/charms/precise/juju-gui/trunk
- Precise Pangolin (12.04)
- use-npm-cache
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 51 |
Proposed branch: | lp:~benji/charms/precise/juju-gui/use-npm-cache |
Merge into: | lp:~juju-gui/charms/precise/juju-gui/trunk |
Diff against target: |
1233 lines (+1076/-22) (has conflicts) 6 files modified
hooks/backend.py (+38/-17) hooks/utils.py (+27/-1) revision (+4/-0) tests/backend.py (+291/-0) tests/test_utils.py (+87/-4) tests/utils.py (+629/-0) Text conflict in revision |
To merge this branch: | bzr merge lp:~benji/charms/precise/juju-gui/use-npm-cache |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Gary Poster (community) | Approve | ||
charmers | Pending | ||
Review via email: mp+161477@code.launchpad.net |
Commit message
Description of the change
Use the npm cache to speed installs from a branch
Creating the cache is handled by the GUI Makefile
Benji York (benji) wrote : | # |
Gary Poster (gary) wrote : | # |
Thanks for the docs and comments! They are great to have.
29 + # If the given installable thing ("backend") requires one or more debs
30 + # that are not yet installed, install them.
31 missing = backend.
Yeah, check_packages might be better named install_
39 + # Inject NPM packages into the cache for faster building.
40 + #prime_
Uh? I hope this wasn't commented out for your tests. :-)
44 + # XXX Why do we only set up the GUI if the "juju-gui-source"
45 + # configuration is non-default?
It's not non-default: it is not-what-
124 + if e.errno != 17: # File exists.
Maybe use errno.EEXIST instead?
126 + uncompress = command('tar', '-x', '-z', '-C', npm_cache_dir, '-f')
127 + cmd_log(
That reads very nicely. Good idea.
164 + get_npm_
You look like you were on your way to a test? That would have been nice. :-) If not, remove the import. But adding an import of prime_npm_cache and testing them both would be nicer. :-)
Thanks!
Gary
Benji York (benji) wrote : | # |
On Mon, Apr 29, 2013 at 3:31 PM, Gary Poster <email address hidden> wrote:
> Review: Approve
>
> Thanks for the docs and comments! They are great to have.
>
> 29 + # If the given installable thing ("backend") requires one or more debs
> 30 + # that are not yet installed, install them.
> 31 missing = backend.
>
> Yeah, check_packages might be better named install_
> something like that.
Yep (I was only commenting things at the time, but this looks like a
local-enough change that I'll go ahead and do it).
> 39 + # Inject NPM packages into the cache for faster building.
> 40 + #prime_
>
> Uh? I hope this wasn't commented out for your tests. :-)
Heh. Yeah, I was doing some non-cache timings and forgot to put it
back. Fixed.
> 44 + # XXX Why do we only set up the GUI if the "juju-gui-source"
> 45 + # configuration is non-default?
>
> It's not non-default: it is not-what-
> the intent. I think that's the behavior too, or we'd be in trouble.
> :-) At the start, what-it-was-before is some empty value, I believe.
Ah! I think you're right. Maybe I can come up with variable/method
names that communicate better; or a comment at least.
> 124 + if e.errno != 17: # File exists.
>
> Maybe use errno.EEXIST instead?
Good call. Will do.
> 126 + uncompress = command('tar', '-x', '-z', '-C', npm_cache_dir, '-f')
> 127 + cmd_log(
>
> That reads very nicely. Good idea.
I thought so too. That's why I copied it from another part of the code. ;)
> 164 + get_npm_
>
> You look like you were on your way to a test? That would have been
> nice. :-)
Indeed. I thought I had added it. I'll do so.
--
Benji York
- 49. By Benji York
-
review fixes
Benji York (benji) wrote : | # |
Please take a look.
Benji York (benji) wrote : | # |
*** Submitted:
Use the npm cache to speed installs from a branch
Creating the cache is handled by the GUI Makefile
Preview Diff
1 | === modified file 'hooks/backend.py' |
2 | --- hooks/backend.py 2013-04-30 13:53:20 +0000 |
3 | +++ hooks/backend.py 2013-04-30 14:57:26 +0000 |
4 | @@ -29,17 +29,19 @@ |
5 | IMPROV, |
6 | JUJU_DIR, |
7 | chain, |
8 | - check_packages, |
9 | + find_missing_packages, |
10 | cmd_log, |
11 | fetch_api, |
12 | fetch_gui, |
13 | get_config, |
14 | + get_npm_cache_archive_url, |
15 | legacy_juju, |
16 | merge, |
17 | overrideable, |
18 | + prime_npm_cache, |
19 | save_or_create_certificates, |
20 | + setup_apache, |
21 | setup_gui, |
22 | - setup_apache, |
23 | start_agent, |
24 | start_gui, |
25 | start_improv, |
26 | @@ -53,20 +55,40 @@ |
27 | |
28 | |
29 | class InstallMixin(object): |
30 | + """Provide for the GUI and its dependencies to be installed.""" |
31 | + |
32 | def install(self, backend): |
33 | + """Install the GUI and dependencies.""" |
34 | config = backend.config |
35 | - missing = backend.check_packages(*backend.debs) |
36 | + # If the given installable thing ("backend") requires one or more debs |
37 | + # that are not yet installed, install them. |
38 | + missing = backend.find_missing_packages(*backend.debs) |
39 | if missing: |
40 | cmd_log(backend.install_extra_repositories(*backend.repositories)) |
41 | cmd_log(apt_get_install(*backend.debs)) |
42 | |
43 | + # If we are not using a pre-built release of the GUI (i.e., we are |
44 | + # using a branch) then we need to build a release archive to use. |
45 | if backend.different('juju-gui-source'): |
46 | - release_tarball = fetch_gui( |
47 | - config['juju-gui-source'], config['command-log-file']) |
48 | - setup_gui(release_tarball) |
49 | + # Inject NPM packages into the cache for faster building. |
50 | + self._prime_npm_cache() |
51 | + # Build a release from the branch. |
52 | + self._build_and_install_from_branch() |
53 | + |
54 | + def _prime_npm_cache(self): |
55 | + # This is a separate method so it can be easily overridden for testing. |
56 | + prime_npm_cache(get_npm_cache_archive_url()) |
57 | + |
58 | + def _build_and_install_from_branch(self): |
59 | + # This is a separate method so it can be easily overridden for testing. |
60 | + release_tarball = fetch_gui( |
61 | + config['juju-gui-source'], config['command-log-file']) |
62 | + setup_gui(release_tarball) |
63 | |
64 | |
65 | class UpstartMixin(object): |
66 | + """Manage (install, start, stop, etc.) some service via Upstart.""" |
67 | + |
68 | upstart_scripts = ('haproxy.conf', ) |
69 | debs = ('curl', 'openssl', 'haproxy', 'apache2') |
70 | |
71 | @@ -152,16 +174,14 @@ |
72 | |
73 | |
74 | class Backend(object): |
75 | - """Compose methods and policy needed to interact |
76 | - with a Juju backend. Given a config dict (which typically |
77 | - comes from the JSON de-serialization of config.json in JujuGUI). |
78 | + """Compose methods and policy needed to interact with a Juju backend. |
79 | + |
80 | + "config" is a config dict (which typically comes from the JSON |
81 | + de-serialization of config.json in JujuGUI). |
82 | """ |
83 | |
84 | def __init__(self, config=None, prev_config=None, **overrides): |
85 | - """ |
86 | - Backends function through composition. __init__ becomes the |
87 | - factory method to generate a selection of strategy classes |
88 | - to use together to implement the backend proper. |
89 | + """Generate a selection of strategy classes that implement the backend. |
90 | """ |
91 | # Ingest the config and build out the ordered list of |
92 | # backend elements to include |
93 | @@ -171,7 +191,7 @@ |
94 | self.prev_config = prev_config |
95 | self.overrides = overrides |
96 | |
97 | - # We always use upstart. |
98 | + # We always install the GUI. |
99 | backends = [InstallMixin, ] |
100 | |
101 | api = "python" if legacy_juju() else "go" |
102 | @@ -194,7 +214,8 @@ |
103 | "Unable to use sandbox with {} backend".format(api)) |
104 | backends.append(GoBackend) |
105 | |
106 | - # All backends can manage the gui. |
107 | + # All backends need to install, start, and stop the services that |
108 | + # provide the GUI. |
109 | backends.append(GuiMixin) |
110 | backends.append(UpstartMixin) |
111 | |
112 | @@ -213,8 +234,8 @@ |
113 | raise |
114 | |
115 | @overrideable |
116 | - def check_packages(self, *packages): |
117 | - return check_packages(*packages) |
118 | + def find_missing_packages(self, *packages): |
119 | + return find_missing_packages(*packages) |
120 | |
121 | @overrideable |
122 | def service_control(self, service, action): |
123 | |
124 | === modified file 'hooks/utils.py' |
125 | --- hooks/utils.py 2013-04-30 13:53:20 +0000 |
126 | +++ hooks/utils.py 2013-04-30 14:57:26 +0000 |
127 | @@ -404,6 +404,32 @@ |
128 | render_to_file('apache-site.template', context, JUJU_GUI_SITE) |
129 | |
130 | |
131 | +def get_npm_cache_archive_url(Launchpad=Launchpad): |
132 | + """Figure out the URL of the most recent NPM cache archive on Launchpad.""" |
133 | + launchpad = Launchpad.login_anonymously('Juju GUI charm', 'production') |
134 | + project = launchpad.projects['juju-gui'] |
135 | + # Find the URL of the most recently created NPM cache archive. |
136 | + npm_cache_url = get_release_file_url(project, 'npm-cache', None) |
137 | + return npm_cache_url |
138 | + |
139 | + |
140 | +def prime_npm_cache(npm_cache_url): |
141 | + """Download NPM cache archive and prime the NPM cache with it.""" |
142 | + # Download the cache archive and then uncompress it into the NPM cache. |
143 | + npm_cache_archive = os.path.join(CURRENT_DIR, 'npm-cache.tgz') |
144 | + cmd_log(run('curl', '-L', '-o', npm_cache_archive, npm_cache_url)) |
145 | + npm_cache_dir = os.path.expanduser('~/.npm') |
146 | + # The NPM cache directory probably does not exist, so make it if not. |
147 | + try: |
148 | + os.mkdir(npm_cache_dir) |
149 | + except OSError, e: |
150 | + # If the directory already exists then ignore the error. |
151 | + if e.errno != errno.EEXIST: # File exists. |
152 | + raise |
153 | + uncompress = command('tar', '-x', '-z', '-C', npm_cache_dir, '-f') |
154 | + cmd_log(uncompress(npm_cache_archive)) |
155 | + |
156 | + |
157 | def fetch_gui(juju_gui_source, logpath): |
158 | """Retrieve the Juju GUI release/branch.""" |
159 | # Retrieve a Juju GUI release. |
160 | @@ -520,7 +546,7 @@ |
161 | shutil.copyfileobj(open(crt_path), pem_file) |
162 | |
163 | |
164 | -def check_packages(*packages): |
165 | +def find_missing_packages(*packages): |
166 | """Given a list of packages, return the packages which are not installed. |
167 | """ |
168 | cache = apt.Cache() |
169 | |
170 | === modified file 'revision' |
171 | --- revision 2013-04-30 13:53:20 +0000 |
172 | +++ revision 2013-04-30 14:57:26 +0000 |
173 | @@ -1,1 +1,5 @@ |
174 | +<<<<<<< TREE |
175 | 41 |
176 | +======= |
177 | +44 |
178 | +>>>>>>> MERGE-SOURCE |
179 | |
180 | === modified symlink 'tests/backend.py' |
181 | === target was u'../hooks/backend.py' |
182 | --- tests/backend.py 1970-01-01 00:00:00 +0000 |
183 | +++ tests/backend.py 2013-04-30 14:57:26 +0000 |
184 | @@ -0,0 +1,291 @@ |
185 | +""" |
186 | +A composition system for creating backend object. |
187 | + |
188 | +Backends implement start(), stop() and install() methods. A backend is composed |
189 | +of many mixins and each mixin will implement any/all of those methods and all |
190 | +will be called. Backends additionally provide for collecting property values |
191 | +from each mixin into a single final property on the backend. There is also a |
192 | +feature for determining if configuration values have changed between old and |
193 | +new configurations so we can selectively take action. |
194 | +""" |
195 | + |
196 | +from charmhelpers import ( |
197 | + RESTART, |
198 | + STOP, |
199 | + log, |
200 | + open_port, |
201 | + service_control, |
202 | +) |
203 | +from shelltoolbox import ( |
204 | + apt_get_install, |
205 | + command, |
206 | + install_extra_repositories, |
207 | + su, |
208 | +) |
209 | +from utils import ( |
210 | + AGENT, |
211 | + APACHE, |
212 | + HAPROXY, |
213 | + IMPROV, |
214 | + JUJU_DIR, |
215 | + chain, |
216 | + find_missing_packages, |
217 | + cmd_log, |
218 | + fetch_api, |
219 | + fetch_gui, |
220 | + get_config, |
221 | + get_npm_cache_archive_url, |
222 | + legacy_juju, |
223 | + merge, |
224 | + overrideable, |
225 | + prime_npm_cache, |
226 | + save_or_create_certificates, |
227 | + setup_apache, |
228 | + setup_gui, |
229 | + start_agent, |
230 | + start_gui, |
231 | + start_improv, |
232 | +) |
233 | + |
234 | +import os |
235 | +import shutil |
236 | + |
237 | + |
238 | +apt_get = command('apt-get') |
239 | + |
240 | + |
241 | +class InstallMixin(object): |
242 | + """Provide for the GUI and its dependencies to be installed.""" |
243 | + |
244 | + def install(self, backend): |
245 | + """Install the GUI and dependencies.""" |
246 | + config = backend.config |
247 | + # If the given installable thing ("backend") requires one or more debs |
248 | + # that are not yet installed, install them. |
249 | + missing = backend.find_missing_packages(*backend.debs) |
250 | + if missing: |
251 | + cmd_log(backend.install_extra_repositories(*backend.repositories)) |
252 | + cmd_log(apt_get_install(*backend.debs)) |
253 | + |
254 | + # If we are not using a pre-built release of the GUI (i.e., we are |
255 | + # using a branch) then we need to build a release archive to use. |
256 | + if backend.different('juju-gui-source'): |
257 | + # Inject NPM packages into the cache for faster building. |
258 | + self._prime_npm_cache() |
259 | + # Build a release from the branch. |
260 | + self._build_and_install_from_branch() |
261 | + |
262 | + def _prime_npm_cache(self): |
263 | + # This is a separate method so it can be easily overridden for testing. |
264 | + prime_npm_cache(get_npm_cache_archive_url()) |
265 | + |
266 | + def _build_and_install_from_branch(self): |
267 | + # This is a separate method so it can be easily overridden for testing. |
268 | + release_tarball = fetch_gui( |
269 | + config['juju-gui-source'], config['command-log-file']) |
270 | + setup_gui(release_tarball) |
271 | + |
272 | + |
273 | +class UpstartMixin(object): |
274 | + """Manage (install, start, stop, etc.) some service via Upstart.""" |
275 | + |
276 | + upstart_scripts = ('haproxy.conf', ) |
277 | + debs = ('curl', 'openssl', 'haproxy', 'apache2') |
278 | + |
279 | + def install(self, backend): |
280 | + """Set up haproxy and nginx upstart configuration files.""" |
281 | + setup_apache() |
282 | + backend.log('Setting up haproxy and nginx start up scripts.') |
283 | + config = backend.config |
284 | + if backend.different( |
285 | + 'ssl-cert-path', 'ssl-cert-contents', 'ssl-key-contents'): |
286 | + save_or_create_certificates( |
287 | + config['ssl-cert-path'], config.get('ssl-cert-contents'), |
288 | + config.get('ssl-key-contents')) |
289 | + |
290 | + source_dir = os.path.join(os.path.dirname(__file__), '..', 'config') |
291 | + for config_file in backend.upstart_scripts: |
292 | + shutil.copy(os.path.join(source_dir, config_file), '/etc/init/') |
293 | + |
294 | + def start(self, backend): |
295 | + with su('root'): |
296 | + backend.service_control(APACHE, RESTART) |
297 | + backend.service_control(HAPROXY, RESTART) |
298 | + |
299 | + def stop(self, backend): |
300 | + with su('root'): |
301 | + backend.service_control(HAPROXY, STOP) |
302 | + backend.service_control(APACHE, STOP) |
303 | + |
304 | + |
305 | +class GuiMixin(object): |
306 | + gui_properties = set([ |
307 | + 'juju-gui-console-enabled', 'login-help', 'read-only', |
308 | + 'serve-tests', 'secure']) |
309 | + |
310 | + repositories = ('ppa:juju-gui/ppa',) |
311 | + |
312 | + def start(self, config): |
313 | + start_gui( |
314 | + config['juju-gui-console-enabled'], config['login-help'], |
315 | + config['read-only'], config['staging'], config['ssl-cert-path'], |
316 | + config['charmworld-url'], config['serve-tests'], |
317 | + secure=config['secure'], sandbox=config['sandbox']) |
318 | + open_port(80) |
319 | + open_port(443) |
320 | + |
321 | + |
322 | +class SandboxBackend(object): |
323 | + pass |
324 | + |
325 | + |
326 | +class PythonBackend(object): |
327 | + |
328 | + def install(self, config): |
329 | + if (not os.path.exists(JUJU_DIR) or |
330 | + config.different('staging', 'juju-api-branch')): |
331 | + fetch_api(config['juju-api-branch']) |
332 | + |
333 | + def start(self, backend): |
334 | + backend.start_agent(backend['ssl-cert-path']) |
335 | + |
336 | + def stop(self, backend): |
337 | + backend.service_control(AGENT, STOP) |
338 | + |
339 | + |
340 | +class ImprovBackend(object): |
341 | + debs = ('zookeeper', ) |
342 | + |
343 | + def install(self, config): |
344 | + if (not os.path.exists(JUJU_DIR) or |
345 | + config.different('staging', 'juju-api-branch')): |
346 | + fetch_api(config['juju-api-branch']) |
347 | + |
348 | + def start(self, backend): |
349 | + backend.start_improv( |
350 | + backend['staging-environment'], backend['ssl-cert-path']) |
351 | + |
352 | + def stop(self, backend): |
353 | + backend.service_control(IMPROV, STOP) |
354 | + |
355 | + |
356 | +class GoBackend(object): |
357 | + debs = ('python-yaml', ) |
358 | + |
359 | + |
360 | +class Backend(object): |
361 | + """Compose methods and policy needed to interact with a Juju backend. |
362 | + |
363 | + "config" is a config dict (which typically comes from the JSON |
364 | + de-serialization of config.json in JujuGUI). |
365 | + """ |
366 | + |
367 | + def __init__(self, config=None, prev_config=None, **overrides): |
368 | + """Generate a selection of strategy classes that implement the backend. |
369 | + """ |
370 | + # Ingest the config and build out the ordered list of |
371 | + # backend elements to include |
372 | + if config is None: |
373 | + config = get_config() |
374 | + self.config = config |
375 | + self.prev_config = prev_config |
376 | + self.overrides = overrides |
377 | + |
378 | + # We always install the GUI. |
379 | + backends = [InstallMixin, ] |
380 | + |
381 | + api = "python" if legacy_juju() else "go" |
382 | + sandbox = config.get('sandbox', False) |
383 | + staging = config.get('staging', False) |
384 | + |
385 | + if api == 'python': |
386 | + if staging: |
387 | + backends.append(ImprovBackend) |
388 | + elif sandbox: |
389 | + backends.append(SandboxBackend) |
390 | + else: |
391 | + backends.append(PythonBackend) |
392 | + else: |
393 | + if staging: |
394 | + raise ValueError( |
395 | + "Unable to use staging with {} backend".format(api)) |
396 | + if sandbox: |
397 | + raise ValueError( |
398 | + "Unable to use sandbox with {} backend".format(api)) |
399 | + backends.append(GoBackend) |
400 | + |
401 | + # All backends need to install, start, and stop the services that |
402 | + # provide the GUI. |
403 | + backends.append(GuiMixin) |
404 | + backends.append(UpstartMixin) |
405 | + |
406 | + # record our choice mapping classes to instances |
407 | + for i, b in enumerate(backends): |
408 | + if callable(b): |
409 | + backends[i] = b() |
410 | + self.backends = backends |
411 | + |
412 | + def __getitem__(self, key): |
413 | + try: |
414 | + return self.config[key] |
415 | + except KeyError: |
416 | + print("Unable to extract config key '%s' from %s" % |
417 | + (key, self.config)) |
418 | + raise |
419 | + |
420 | + @overrideable |
421 | + def find_missing_packages(self, *packages): |
422 | + return find_missing_packages(*packages) |
423 | + |
424 | + @overrideable |
425 | + def service_control(self, service, action): |
426 | + service_control(service, action) |
427 | + |
428 | + @overrideable |
429 | + def start_agent(self, cert_path): |
430 | + start_agent(cert_path) |
431 | + |
432 | + @overrideable |
433 | + def start_improv(self, stage_env, cert_path): |
434 | + start_improv(stage_env, cert_path) |
435 | + |
436 | + @overrideable |
437 | + def log(self, msg, *args): |
438 | + log(msg, *args) |
439 | + |
440 | + @overrideable |
441 | + def install_extra_repositories(self, *packages): |
442 | + if self.config.get('allow-additional-deb-repositories', True): |
443 | + install_extra_repositories(*packages) |
444 | + else: |
445 | + apt_get('update') |
446 | + |
447 | + def different(self, *keys): |
448 | + """Return a boolean indicating if the current config |
449 | + value differs from the config value passed in prev_config |
450 | + with respect to any of the passed in string keys. |
451 | + """ |
452 | + if self.prev_config is None: |
453 | + return True |
454 | + |
455 | + for key in keys: |
456 | + current = self.config.get(key) |
457 | + prev = self.prev_config.get(key) |
458 | + r = current != prev |
459 | + if r: |
460 | + return True |
461 | + return False |
462 | + |
463 | + ## Composed Methods |
464 | + install = chain('install') |
465 | + start = chain('start') |
466 | + stop = chain('stop') |
467 | + |
468 | + ## Merged Properties |
469 | + dependencies = merge('dependencies') |
470 | + build_dependencies = merge('build_dependencies') |
471 | + staging_dependencies = merge('staging_dependencies') |
472 | + |
473 | + repositories = merge('repositories') |
474 | + debs = merge('debs') |
475 | + upstart_scripts = merge('upstart_scripts') |
476 | |
477 | === modified file 'tests/test_utils.py' |
478 | --- tests/test_utils.py 2013-04-28 00:59:29 +0000 |
479 | +++ tests/test_utils.py 2013-04-30 14:57:26 +0000 |
480 | @@ -12,25 +12,27 @@ |
481 | import charmhelpers |
482 | import yaml |
483 | |
484 | +from backend import InstallMixin |
485 | from utils import ( |
486 | + API_PORT, |
487 | + JUJU_GUI_DIR, |
488 | + JUJU_PEM, |
489 | + WEB_PORT, |
490 | _get_by_attr, |
491 | - API_PORT, |
492 | cmd_log, |
493 | first_path_in_dir, |
494 | get_api_address, |
495 | get_release_file_url, |
496 | get_zookeeper_address, |
497 | - JUJU_GUI_DIR, |
498 | - JUJU_PEM, |
499 | legacy_juju, |
500 | log_hook, |
501 | parse_source, |
502 | + get_npm_cache_archive_url, |
503 | render_to_file, |
504 | save_or_create_certificates, |
505 | start_agent, |
506 | start_gui, |
507 | start_improv, |
508 | - WEB_PORT, |
509 | ) |
510 | # Import the whole utils package for monkey patching. |
511 | import utils |
512 | @@ -603,5 +605,86 @@ |
513 | self.assertNotIn('redirect scheme https', haproxy_conf) |
514 | |
515 | |
516 | +class TestNpmCache(unittest.TestCase): |
517 | + """To speed building from a branch we prepopulate the NPM cache.""" |
518 | + |
519 | + def test_retrieving_cache_url(self): |
520 | + # The URL for the latest cache file can be retrieved from Launchpad. |
521 | + class FauxLaunchpadFactory(object): |
522 | + @staticmethod |
523 | + def login_anonymously(agent, site): |
524 | + # We download the cache from the production site. |
525 | + self.assertEqual(site, 'production') |
526 | + return FauxLaunchpad |
527 | + |
528 | + class CacheFile(object): |
529 | + file_link = 'http://launchpad.example/path/to/cache/file' |
530 | + |
531 | + def __str__(self): |
532 | + return 'cache-file-123.tgz' |
533 | + |
534 | + class NpmRelease(object): |
535 | + files = [CacheFile()] |
536 | + |
537 | + class NpmSeries(object): |
538 | + name = 'npm-cache' |
539 | + releases = [NpmRelease] |
540 | + |
541 | + class FauxProject(object): |
542 | + series = [NpmSeries] |
543 | + |
544 | + class FauxLaunchpad(object): |
545 | + projects = {'juju-gui': FauxProject()} |
546 | + |
547 | + url = get_npm_cache_archive_url(Launchpad=FauxLaunchpadFactory()) |
548 | + self.assertEqual(url, 'http://launchpad.example/path/to/cache/file') |
549 | + |
550 | + |
551 | + def test_InstallMixin_primes_npm_cache(self): |
552 | + # The InstallMixin.install() method primes the NPM cache before |
553 | + # building (and installing) the GUI from a branch. |
554 | + assertFalse = self.assertFalse |
555 | + assertTrue = self.assertTrue |
556 | + |
557 | + class TestableInstallMixin(InstallMixin): |
558 | + """An InstallMixin that records actions instead of taking them.""" |
559 | + cache_primed = False |
560 | + branch_installed = False |
561 | + |
562 | + def _prime_npm_cache(self): |
563 | + # The cache is primed before the branch is installed. |
564 | + assertFalse(self.branch_installed) |
565 | + self.cache_primed = True |
566 | + |
567 | + def _build_and_install_from_branch(self): |
568 | + # The cache is primed before the branch is installed. |
569 | + assertTrue(self.cache_primed) |
570 | + self.branch_installed = True |
571 | + |
572 | + class FauxBackend(object): |
573 | + """A test backend.""" |
574 | + config = None |
575 | + debs = ('DEBS',) |
576 | + |
577 | + @classmethod |
578 | + def find_missing_packages(cls, *debs): |
579 | + self.assertEqual(cls.debs, debs) |
580 | + return False |
581 | + |
582 | + @classmethod |
583 | + def different(cls, key): |
584 | + self.assertEqual(key, 'juju-gui-source') |
585 | + return True |
586 | + |
587 | + mixin = TestableInstallMixin() |
588 | + # Prior to "install" the NPM cache has not been primed. |
589 | + self.assertFalse(mixin.cache_primed) |
590 | + self.assertFalse(mixin.branch_installed) |
591 | + mixin.install(FauxBackend()) |
592 | + # After "install" the NPM cache has been primed. |
593 | + self.assertTrue(mixin.cache_primed) |
594 | + self.assertTrue(mixin.branch_installed) |
595 | + |
596 | + |
597 | if __name__ == '__main__': |
598 | unittest.main(verbosity=2) |
599 | |
600 | === modified symlink 'tests/utils.py' |
601 | === target was u'../hooks/utils.py' |
602 | --- tests/utils.py 1970-01-01 00:00:00 +0000 |
603 | +++ tests/utils.py 2013-04-30 14:57:26 +0000 |
604 | @@ -0,0 +1,629 @@ |
605 | +"""Juju GUI charm utilities.""" |
606 | + |
607 | +__all__ = [ |
608 | + 'AGENT', |
609 | + 'APACHE', |
610 | + 'API_PORT', |
611 | + 'CURRENT_DIR', |
612 | + 'HAPROXY', |
613 | + 'IMPROV', |
614 | + 'JUJU_DIR', |
615 | + 'JUJU_GUI_DIR', |
616 | + 'JUJU_GUI_SITE', |
617 | + 'JUJU_PEM', |
618 | + 'StopChain', |
619 | + 'WEB_PORT', |
620 | + 'bzr_checkout', |
621 | + 'chain', |
622 | + 'cmd_log', |
623 | + 'fetch_api', |
624 | + 'fetch_gui', |
625 | + 'first_path_in_dir', |
626 | + 'get_api_address', |
627 | + 'get_release_file_url', |
628 | + 'get_staging_dependencies', |
629 | + 'get_zookeeper_address', |
630 | + 'legacy_juju', |
631 | + 'log_hook', |
632 | + 'merge', |
633 | + 'overrideable', |
634 | + 'parse_source', |
635 | + 'render_to_file', |
636 | + 'save_or_create_certificates', |
637 | + 'setup_apache', |
638 | + 'setup_gui', |
639 | + 'start_agent', |
640 | + 'start_gui', |
641 | + 'start_improv', |
642 | + 'write_apache_config', |
643 | +] |
644 | + |
645 | +from contextlib import contextmanager |
646 | +import json |
647 | +import os |
648 | +import logging |
649 | +import shutil |
650 | +from subprocess import CalledProcessError |
651 | +import tempfile |
652 | +from urlparse import urlparse |
653 | + |
654 | +from launchpadlib.launchpad import Launchpad |
655 | +from shelltoolbox import ( |
656 | + Serializer, |
657 | + apt_get_install, |
658 | + command, |
659 | + environ, |
660 | + install_extra_repositories, |
661 | + run, |
662 | + script_name, |
663 | + search_file, |
664 | + su, |
665 | +) |
666 | +from charmhelpers import ( |
667 | + START, |
668 | + get_config, |
669 | + log, |
670 | + service_control, |
671 | + unit_get, |
672 | +) |
673 | + |
674 | +import apt |
675 | +import tempita |
676 | + |
677 | + |
678 | +AGENT = 'juju-api-agent' |
679 | +APACHE = 'apache2' |
680 | +IMPROV = 'juju-api-improv' |
681 | +HAPROXY = 'haproxy' |
682 | + |
683 | +API_PORT = 8080 |
684 | +WEB_PORT = 8000 |
685 | + |
686 | +CURRENT_DIR = os.getcwd() |
687 | +JUJU_DIR = os.path.join(CURRENT_DIR, 'juju') |
688 | +JUJU_GUI_DIR = os.path.join(CURRENT_DIR, 'juju-gui') |
689 | +JUJU_GUI_SITE = '/etc/apache2/sites-available/juju-gui' |
690 | +JUJU_GUI_PORTS = '/etc/apache2/ports.conf' |
691 | +JUJU_PEM = 'juju.includes-private-key.pem' |
692 | +BUILD_REPOSITORIES = ('ppa:chris-lea/node.js-legacy',) |
693 | +DEB_BUILD_DEPENDENCIES = ( |
694 | + 'bzr', 'imagemagick', 'make', 'nodejs', 'npm', |
695 | +) |
696 | +DEB_STAGE_DEPENDENCIES = ( |
697 | + 'zookeeper', |
698 | +) |
699 | + |
700 | + |
701 | +# Store the configuration from on invocation to the next. |
702 | +config_json = Serializer('/tmp/config.json') |
703 | +# Bazaar checkout command. |
704 | +bzr_checkout = command('bzr', 'co', '--lightweight') |
705 | +# Whether or not the charm is deployed using juju-core. |
706 | +# If juju-core has been used to deploy the charm, an agent.conf file must |
707 | +# be present in the charm parent directory. |
708 | +legacy_juju = lambda: not os.path.exists( |
709 | + os.path.join(CURRENT_DIR, '..', 'agent.conf')) |
710 | + |
711 | + |
712 | +def _get_build_dependencies(): |
713 | + """Install deb dependencies for building.""" |
714 | + log('Installing build dependencies.') |
715 | + cmd_log(install_extra_repositories(*BUILD_REPOSITORIES)) |
716 | + cmd_log(apt_get_install(*DEB_BUILD_DEPENDENCIES)) |
717 | + |
718 | + |
719 | +def get_api_address(unit_dir): |
720 | + """Return the Juju API address stored in the uniter agent.conf file.""" |
721 | + import yaml # python-yaml is only installed if juju-core is used. |
722 | + # XXX 2013-03-27 frankban bug=1161443: |
723 | + # currently the uniter agent.conf file does not include the API |
724 | + # address. For now retrieve it from the machine agent file. |
725 | + base_dir = os.path.abspath(os.path.join(unit_dir, '..')) |
726 | + for dirname in os.listdir(base_dir): |
727 | + if dirname.startswith('machine-'): |
728 | + agent_conf = os.path.join(base_dir, dirname, 'agent.conf') |
729 | + break |
730 | + else: |
731 | + raise IOError('Juju agent configuration file not found.') |
732 | + contents = yaml.load(open(agent_conf)) |
733 | + return contents['apiinfo']['addrs'][0] |
734 | + |
735 | + |
736 | +def get_staging_dependencies(): |
737 | + """Install deb dependencies for the stage (improv) environment.""" |
738 | + log('Installing stage dependencies.') |
739 | + cmd_log(apt_get_install(*DEB_STAGE_DEPENDENCIES)) |
740 | + |
741 | + |
742 | +def first_path_in_dir(directory): |
743 | + """Return the full path of the first file/dir in *directory*.""" |
744 | + return os.path.join(directory, os.listdir(directory)[0]) |
745 | + |
746 | + |
747 | +def _get_by_attr(collection, attr, value): |
748 | + """Return the first item in collection having attr == value. |
749 | + |
750 | + Return None if the item is not found. |
751 | + """ |
752 | + for item in collection: |
753 | + if getattr(item, attr) == value: |
754 | + return item |
755 | + |
756 | + |
757 | +def get_release_file_url(project, series_name, release_version): |
758 | + """Return the URL of the release file hosted in Launchpad. |
759 | + |
760 | + The returned URL points to a release file for the given project, series |
761 | + name and release version. |
762 | + The argument *project* is a project object as returned by launchpadlib. |
763 | + The arguments *series_name* and *release_version* are strings. If |
764 | + *release_version* is None, the URL of the latest release will be returned. |
765 | + """ |
766 | + series = _get_by_attr(project.series, 'name', series_name) |
767 | + if series is None: |
768 | + raise ValueError('%r: series not found' % series_name) |
769 | + # Releases are returned by Launchpad in reverse date order. |
770 | + releases = list(series.releases) |
771 | + if not releases: |
772 | + raise ValueError('%r: series does not contain releases' % series_name) |
773 | + if release_version is not None: |
774 | + release = _get_by_attr(releases, 'version', release_version) |
775 | + if release is None: |
776 | + raise ValueError('%r: release not found' % release_version) |
777 | + releases = [release] |
778 | + for release in releases: |
779 | + for file_ in release.files: |
780 | + if str(file_).endswith('.tgz'): |
781 | + return file_.file_link |
782 | + raise ValueError('%r: file not found' % release_version) |
783 | + |
784 | + |
785 | +def get_zookeeper_address(agent_file_path): |
786 | + """Retrieve the Zookeeper address contained in the given *agent_file_path*. |
787 | + |
788 | + The *agent_file_path* is a path to a file containing a line similar to the |
789 | + following:: |
790 | + |
791 | + env JUJU_ZOOKEEPER="address" |
792 | + """ |
793 | + line = search_file('JUJU_ZOOKEEPER', agent_file_path).strip() |
794 | + return line.split('=')[1].strip('"') |
795 | + |
796 | + |
797 | +@contextmanager |
798 | +def log_hook(): |
799 | + """Log when a hook starts and stops its execution. |
800 | + |
801 | + Also log to stdout possible CalledProcessError exceptions raised executing |
802 | + the hook. |
803 | + """ |
804 | + script = script_name() |
805 | + log(">>> Entering {}".format(script)) |
806 | + try: |
807 | + yield |
808 | + except CalledProcessError as err: |
809 | + log('Exception caught:') |
810 | + log(err.output) |
811 | + raise |
812 | + finally: |
813 | + log("<<< Exiting {}".format(script)) |
814 | + |
815 | + |
816 | +def parse_source(source): |
817 | + """Parse the ``juju-gui-source`` option. |
818 | + |
819 | + Return a tuple of two elements representing info on how to deploy Juju GUI. |
820 | + Examples: |
821 | + - ('stable', None): latest stable release; |
822 | + - ('stable', '0.1.0'): stable release v0.1.0; |
823 | + - ('trunk', None): latest trunk release; |
824 | + - ('trunk', '0.1.0+build.1'): trunk release v0.1.0 bzr revision 1; |
825 | + - ('branch', 'lp:juju-gui'): release is made from a branch; |
826 | + - ('url', 'http://example.com/gui'): release from a downloaded file. |
827 | + """ |
828 | + if source.startswith('url:'): |
829 | + source = source[4:] |
830 | + # Support file paths, including relative paths. |
831 | + if urlparse(source).scheme == '': |
832 | + if not source.startswith('/'): |
833 | + source = os.path.join(os.path.abspath(CURRENT_DIR), source) |
834 | + source = "file://%s" % source |
835 | + return 'url', source |
836 | + if source in ('stable', 'trunk'): |
837 | + return source, None |
838 | + if source.startswith('lp:') or source.startswith('http://'): |
839 | + return 'branch', source |
840 | + if 'build' in source: |
841 | + return 'trunk', source |
842 | + return 'stable', source |
843 | + |
844 | + |
845 | +def render_to_file(template_name, context, destination): |
846 | + """Render the given *template_name* into *destination* using *context*. |
847 | + |
848 | + The tempita template language is used to render contents |
849 | + (see http://pythonpaste.org/tempita/). |
850 | + The argument *template_name* is the name or path of the template file: |
851 | + it may be either a path relative to ``../config`` or an absolute path. |
852 | + The argument *destination* is a file path. |
853 | + The argument *context* is a dict-like object. |
854 | + """ |
855 | + template_path = os.path.join( |
856 | + os.path.dirname(__file__), '..', 'config', template_name) |
857 | + template = tempita.Template.from_filename(template_path) |
858 | + with open(destination, 'w') as stream: |
859 | + stream.write(template.substitute(context)) |
860 | + |
861 | + |
862 | +results_log = None |
863 | + |
864 | + |
865 | +def _setupLogging(): |
866 | + global results_log |
867 | + if results_log is not None: |
868 | + return |
869 | + config = get_config() |
870 | + logging.basicConfig( |
871 | + filename=config['command-log-file'], |
872 | + level=logging.INFO, |
873 | + format="%(asctime)s: %(name)s@%(levelname)s %(message)s") |
874 | + results_log = logging.getLogger('juju-gui') |
875 | + |
876 | + |
877 | +def cmd_log(results): |
878 | + global results_log |
879 | + if not results: |
880 | + return |
881 | + if results_log is None: |
882 | + _setupLogging() |
883 | + # Since 'results' may be multi-line output, start it on a separate line |
884 | + # from the logger timestamp, etc. |
885 | + results_log.info('\n' + results) |
886 | + |
887 | + |
888 | +def start_improv(staging_env, ssl_cert_path, |
889 | + config_path='/etc/init/juju-api-improv.conf'): |
890 | + """Start a simulated juju environment using ``improv.py``.""" |
891 | + log('Setting up staging start up script.') |
892 | + context = { |
893 | + 'juju_dir': JUJU_DIR, |
894 | + 'keys': ssl_cert_path, |
895 | + 'port': API_PORT, |
896 | + 'staging_env': staging_env, |
897 | + } |
898 | + render_to_file('juju-api-improv.conf.template', context, config_path) |
899 | + log('Starting the staging backend.') |
900 | + with su('root'): |
901 | + service_control(IMPROV, START) |
902 | + |
903 | + |
904 | +def start_agent(ssl_cert_path, config_path='/etc/init/juju-api-agent.conf'): |
905 | + """Start the Juju agent and connect to the current environment.""" |
906 | + # Retrieve the Zookeeper address from the start up script. |
907 | + unit_dir = os.path.realpath(os.path.join(CURRENT_DIR, '..')) |
908 | + agent_file = '/etc/init/juju-{0}.conf'.format(os.path.basename(unit_dir)) |
909 | + zookeeper = get_zookeeper_address(agent_file) |
910 | + log('Setting up API agent start up script.') |
911 | + context = { |
912 | + 'juju_dir': JUJU_DIR, |
913 | + 'keys': ssl_cert_path, |
914 | + 'port': API_PORT, |
915 | + 'zookeeper': zookeeper, |
916 | + } |
917 | + render_to_file('juju-api-agent.conf.template', context, config_path) |
918 | + log('Starting API agent.') |
919 | + with su('root'): |
920 | + service_control(AGENT, START) |
921 | + |
922 | + |
923 | +def start_gui( |
924 | + console_enabled, login_help, readonly, in_staging, ssl_cert_path, |
925 | + charmworld_url, serve_tests, haproxy_path='/etc/haproxy/haproxy.cfg', |
926 | + config_js_path=None, secure=True, sandbox=False): |
927 | + """Set up and start the Juju GUI server.""" |
928 | + with su('root'): |
929 | + run('chown', '-R', 'ubuntu:', JUJU_GUI_DIR) |
930 | + # XXX 2013-02-05 frankban bug=1116320: |
931 | + # External insecure resources are still loaded when testing in the |
932 | + # debug environment. For now, switch to the production environment if |
933 | + # the charm is configured to serve tests. |
934 | + if in_staging and not serve_tests: |
935 | + build_dirname = 'build-debug' |
936 | + else: |
937 | + build_dirname = 'build-prod' |
938 | + build_dir = os.path.join(JUJU_GUI_DIR, build_dirname) |
939 | + log('Generating the Juju GUI configuration file.') |
940 | + is_legacy_juju = legacy_juju() |
941 | + user, password = None, None |
942 | + if is_legacy_juju and in_staging: |
943 | + user, password = 'admin', 'admin' |
944 | + else: |
945 | + user, password = None, None |
946 | + |
947 | + api_backend = 'python' if is_legacy_juju else 'go' |
948 | + if secure: |
949 | + protocol = 'wss' |
950 | + else: |
951 | + log('Running in insecure mode! Port 80 will serve unencrypted.') |
952 | + protocol = 'ws' |
953 | + |
954 | + context = { |
955 | + 'raw_protocol': protocol, |
956 | + 'address': unit_get('public-address'), |
957 | + 'console_enabled': json.dumps(console_enabled), |
958 | + 'login_help': json.dumps(login_help), |
959 | + 'password': json.dumps(password), |
960 | + 'api_backend': json.dumps(api_backend), |
961 | + 'readonly': json.dumps(readonly), |
962 | + 'user': json.dumps(user), |
963 | + 'protocol': json.dumps(protocol), |
964 | + 'sandbox': json.dumps(sandbox), |
965 | + 'charmworld_url': json.dumps(charmworld_url), |
966 | + } |
967 | + if config_js_path is None: |
968 | + config_js_path = os.path.join( |
969 | + build_dir, 'juju-ui', 'assets', 'config.js') |
970 | + render_to_file('config.js.template', context, config_js_path) |
971 | + |
972 | + write_apache_config(build_dir, serve_tests) |
973 | + |
974 | + log('Generating haproxy configuration file.') |
975 | + if is_legacy_juju: |
976 | + # The PyJuju API agent is listening on localhost. |
977 | + api_address = '127.0.0.1:{0}'.format(API_PORT) |
978 | + else: |
979 | + # Retrieve the juju-core API server address. |
980 | + api_address = get_api_address(os.path.join(CURRENT_DIR, '..')) |
981 | + context = { |
982 | + 'api_address': api_address, |
983 | + 'api_pem': JUJU_PEM, |
984 | + 'legacy_juju': is_legacy_juju, |
985 | + 'ssl_cert_path': ssl_cert_path, |
986 | + # In PyJuju environments, use the same certificate for both HTTPS and |
987 | + # WebSocket connections. In juju-core the system already has the proper |
988 | + # certificate installed. |
989 | + 'web_pem': JUJU_PEM, |
990 | + 'web_port': WEB_PORT, |
991 | + 'secure': secure |
992 | + } |
993 | + render_to_file('haproxy.cfg.template', context, haproxy_path) |
994 | + log('Starting Juju GUI.') |
995 | + |
996 | +def write_apache_config(build_dir, serve_tests=False): |
997 | + log('Generating the apache site configuration file.') |
998 | + context = { |
999 | + 'port': WEB_PORT, |
1000 | + 'serve_tests': serve_tests, |
1001 | + 'server_root': build_dir, |
1002 | + 'tests_root': os.path.join(JUJU_GUI_DIR, 'test', ''), |
1003 | + } |
1004 | + render_to_file('apache-ports.template', context, JUJU_GUI_PORTS) |
1005 | + render_to_file('apache-site.template', context, JUJU_GUI_SITE) |
1006 | + |
1007 | + |
1008 | +def get_npm_cache_archive_url(Launchpad=Launchpad): |
1009 | + """Figure out the URL of the most recent NPM cache archive on Launchpad.""" |
1010 | + launchpad = Launchpad.login_anonymously('Juju GUI charm', 'production') |
1011 | + project = launchpad.projects['juju-gui'] |
1012 | + # Find the URL of the most recently created NPM cache archive. |
1013 | + npm_cache_url = get_release_file_url(project, 'npm-cache', None) |
1014 | + return npm_cache_url |
1015 | + |
1016 | + |
1017 | +def prime_npm_cache(npm_cache_url): |
1018 | + """Download NPM cache archive and prime the NPM cache with it.""" |
1019 | + # Download the cache archive and then uncompress it into the NPM cache. |
1020 | + npm_cache_archive = os.path.join(CURRENT_DIR, 'npm-cache.tgz') |
1021 | + cmd_log(run('curl', '-L', '-o', npm_cache_archive, npm_cache_url)) |
1022 | + npm_cache_dir = os.path.expanduser('~/.npm') |
1023 | + # The NPM cache directory probably does not exist, so make it if not. |
1024 | + try: |
1025 | + os.mkdir(npm_cache_dir) |
1026 | + except OSError, e: |
1027 | + # If the directory already exists then ignore the error. |
1028 | + if e.errno != 17: # File exists. |
1029 | + raise |
1030 | + uncompress = command('tar', '-x', '-z', '-C', npm_cache_dir, '-f') |
1031 | + cmd_log(uncompress(npm_cache_archive)) |
1032 | + |
1033 | + |
1034 | +def fetch_gui(juju_gui_source, logpath): |
1035 | + """Retrieve the Juju GUI release/branch.""" |
1036 | + # Retrieve a Juju GUI release. |
1037 | + origin, version_or_branch = parse_source(juju_gui_source) |
1038 | + if origin == 'branch': |
1039 | + # Make sure we have the dependencies necessary for us to actually make |
1040 | + # a build. |
1041 | + _get_build_dependencies() |
1042 | + # Create a release starting from a branch. |
1043 | + juju_gui_source_dir = os.path.join(CURRENT_DIR, 'juju-gui-source') |
1044 | + log('Retrieving Juju GUI source checkout from %s.' % version_or_branch) |
1045 | + cmd_log(run('rm', '-rf', juju_gui_source_dir)) |
1046 | + cmd_log(bzr_checkout(version_or_branch, juju_gui_source_dir)) |
1047 | + log('Preparing a Juju GUI release.') |
1048 | + logdir = os.path.dirname(logpath) |
1049 | + fd, name = tempfile.mkstemp(prefix='make-distfile-', dir=logdir) |
1050 | + log('Output from "make distfile" sent to %s' % name) |
1051 | + with environ(NO_BZR='1'): |
1052 | + run('make', '-C', juju_gui_source_dir, 'distfile', |
1053 | + stdout=fd, stderr=fd) |
1054 | + release_tarball = first_path_in_dir( |
1055 | + os.path.join(juju_gui_source_dir, 'releases')) |
1056 | + else: |
1057 | + log('Retrieving Juju GUI release.') |
1058 | + if origin == 'url': |
1059 | + file_url = version_or_branch |
1060 | + else: |
1061 | + # Retrieve a release from Launchpad. |
1062 | + launchpad = Launchpad.login_anonymously( |
1063 | + 'Juju GUI charm', 'production') |
1064 | + project = launchpad.projects['juju-gui'] |
1065 | + file_url = get_release_file_url(project, origin, version_or_branch) |
1066 | + log('Downloading release file from %s.' % file_url) |
1067 | + release_tarball = os.path.join(CURRENT_DIR, 'release.tgz') |
1068 | + cmd_log(run('curl', '-L', '-o', release_tarball, file_url)) |
1069 | + return release_tarball |
1070 | + |
1071 | + |
1072 | +def fetch_api(juju_api_branch): |
1073 | + """Retrieve the Juju branch.""" |
1074 | + # Retrieve Juju API source checkout. |
1075 | + log('Retrieving Juju API source checkout.') |
1076 | + cmd_log(run('rm', '-rf', JUJU_DIR)) |
1077 | + cmd_log(bzr_checkout(juju_api_branch, JUJU_DIR)) |
1078 | + |
1079 | + |
1080 | +def setup_gui(release_tarball): |
1081 | + """Set up Juju GUI.""" |
1082 | + # Uncompress the release tarball. |
1083 | + log('Installing Juju GUI.') |
1084 | + release_dir = os.path.join(CURRENT_DIR, 'release') |
1085 | + cmd_log(run('rm', '-rf', release_dir)) |
1086 | + os.mkdir(release_dir) |
1087 | + uncompress = command('tar', '-x', '-z', '-C', release_dir, '-f') |
1088 | + cmd_log(uncompress(release_tarball)) |
1089 | + # Link the Juju GUI dir to the contents of the release tarball. |
1090 | + cmd_log(run('ln', '-sf', first_path_in_dir(release_dir), JUJU_GUI_DIR)) |
1091 | + |
1092 | + |
1093 | +def setup_apache(): |
1094 | + """Set up apache.""" |
1095 | + log('Setting up apache.') |
1096 | + if not os.path.exists(JUJU_GUI_SITE): |
1097 | + cmd_log(run('touch', JUJU_GUI_SITE)) |
1098 | + cmd_log(run('chown', 'ubuntu:', JUJU_GUI_SITE)) |
1099 | + cmd_log( |
1100 | + run('ln', '-s', JUJU_GUI_SITE, |
1101 | + '/etc/apache2/sites-enabled/juju-gui')) |
1102 | + |
1103 | + if not os.path.exists(JUJU_GUI_PORTS): |
1104 | + cmd_log(run('touch', JUJU_GUI_PORTS)) |
1105 | + cmd_log(run('chown', 'ubuntu:', JUJU_GUI_PORTS)) |
1106 | + |
1107 | + with su('root'): |
1108 | + run('a2dissite', 'default') |
1109 | + run('a2ensite', 'juju-gui') |
1110 | + |
1111 | + |
1112 | +def save_or_create_certificates( |
1113 | + ssl_cert_path, ssl_cert_contents, ssl_key_contents): |
1114 | + """Generate the SSL certificates. |
1115 | + |
1116 | + If both *ssl_cert_contents* and *ssl_key_contents* are provided, use them |
1117 | + as certificates; otherwise, generate them. |
1118 | + |
1119 | + Also create a pem file, suitable for use in the haproxy configuration, |
1120 | + concatenating the key and the certificate files. |
1121 | + """ |
1122 | + crt_path = os.path.join(ssl_cert_path, 'juju.crt') |
1123 | + key_path = os.path.join(ssl_cert_path, 'juju.key') |
1124 | + if not os.path.exists(ssl_cert_path): |
1125 | + os.makedirs(ssl_cert_path) |
1126 | + if ssl_cert_contents and ssl_key_contents: |
1127 | + # Save the provided certificates. |
1128 | + with open(crt_path, 'w') as cert_file: |
1129 | + cert_file.write(ssl_cert_contents) |
1130 | + with open(key_path, 'w') as key_file: |
1131 | + key_file.write(ssl_key_contents) |
1132 | + else: |
1133 | + # Generate certificates. |
1134 | + # See http://superuser.com/questions/226192/openssl-without-prompt |
1135 | + cmd_log(run( |
1136 | + 'openssl', 'req', '-new', '-newkey', 'rsa:4096', |
1137 | + '-days', '365', '-nodes', '-x509', '-subj', |
1138 | + # These are arbitrary test values for the certificate. |
1139 | + '/C=GB/ST=Juju/L=GUI/O=Ubuntu/CN=juju.ubuntu.com', |
1140 | + '-keyout', key_path, '-out', crt_path)) |
1141 | + # Generate the pem file. |
1142 | + pem_path = os.path.join(ssl_cert_path, JUJU_PEM) |
1143 | + if os.path.exists(pem_path): |
1144 | + os.remove(pem_path) |
1145 | + with open(pem_path, 'w') as pem_file: |
1146 | + shutil.copyfileobj(open(key_path), pem_file) |
1147 | + shutil.copyfileobj(open(crt_path), pem_file) |
1148 | + |
1149 | + |
1150 | +def find_missing_packages(*packages): |
1151 | + """Given a list of packages, return the packages which are not installed. |
1152 | + """ |
1153 | + cache = apt.Cache() |
1154 | + missing = set() |
1155 | + for pkg_name in packages: |
1156 | + try: |
1157 | + pkg = cache[pkg_name] |
1158 | + except KeyError: |
1159 | + missing.add(pkg_name) |
1160 | + continue |
1161 | + if pkg.is_installed: |
1162 | + continue |
1163 | + missing.add(pkg_name) |
1164 | + return missing |
1165 | + |
1166 | + |
1167 | +## Backend support decorators |
1168 | +class StopChain(Exception): |
1169 | + """Stop Processing a chain command without raising |
1170 | + another error. |
1171 | + """ |
1172 | + |
1173 | + |
1174 | +def chain(name, reverse=False): |
1175 | + """Helper method to compose a set of strategy objects into |
1176 | + a callable. |
1177 | + |
1178 | + Each method is called in the context of its strategy |
1179 | + instance (normal OOP) and its argument is the Backend |
1180 | + instance. |
1181 | + """ |
1182 | + # chain method calls through all implementing mixins |
1183 | + def method(self): |
1184 | + workingset = self.backends |
1185 | + if reverse: |
1186 | + workingset = reversed(workingset) |
1187 | + for backend in workingset: |
1188 | + call = backend.__class__.__dict__.get(name) |
1189 | + if call: |
1190 | + try: |
1191 | + call(backend, self) |
1192 | + except StopChain: |
1193 | + break |
1194 | + |
1195 | + method.__name__ = name |
1196 | + return method |
1197 | + |
1198 | + |
1199 | +def overrideable(f): |
1200 | + """Helper to support very limited overrides for use in testing. |
1201 | + |
1202 | + def foo(): |
1203 | + return True |
1204 | + b = Backend(foo=foo) |
1205 | + assert b.foo() is True |
1206 | + """ |
1207 | + name = f.__name__ |
1208 | + |
1209 | + def overridden(self, *args, **kwargs): |
1210 | + if name in self.overrides: |
1211 | + return self.overrides[name](*args, **kwargs) |
1212 | + else: |
1213 | + return f(self, *args, **kwargs) |
1214 | + |
1215 | + overridden.__name__ = name |
1216 | + return overridden |
1217 | + |
1218 | + |
1219 | +def merge(name): |
1220 | + """Helper to merge a property from a set of strategy objects |
1221 | + into a unified set. |
1222 | + """ |
1223 | + # return merged property from every providing backend as a set |
1224 | + @property |
1225 | + def method(self): |
1226 | + result = set() |
1227 | + for backend in self.backends: |
1228 | + segment = backend.__class__.__dict__.get(name) |
1229 | + if segment and isinstance(segment, (list, tuple, set)): |
1230 | + result |= set(segment) |
1231 | + |
1232 | + return result |
1233 | + return method |
Please take a look.