Merge lp:~patrick-hetu/charms/precise/python-django/python-rewrite into lp:~charmers/charms/precise/python-django/trunk

Proposed by Patrick Hetu
Status: Merged
Merged at revision: 25
Proposed branch: lp:~patrick-hetu/charms/precise/python-django/python-rewrite
Merge into: lp:~charmers/charms/precise/python-django/trunk
Diff against target: 3076 lines (+2334/-467)
27 files modified
.bzrignore (+5/-0)
README.rst (+287/-35)
TODO (+0/-25)
config.yaml (+126/-85)
copyright (+1/-1)
fabfile.py (+197/-0)
hooks/common.incl (+0/-13)
hooks/config-changed (+0/-137)
hooks/hooks.py (+854/-0)
hooks/install (+0/-61)
hooks/pgsql-relation-changed (+0/-51)
hooks/upgrade-charm (+0/-11)
hooks/website-relation-joined (+0/-14)
hooks/wsgi-relation-joined (+0/-19)
icon.svg (+395/-0)
metadata.yaml (+17/-14)
revision (+1/-1)
templates/cache.tmpl (+10/-0)
templates/cloudfiles.tmpl (+48/-0)
templates/conf_injection.tmpl (+10/-0)
templates/engine.tmpl (+19/-0)
templates/mongodb_engine.tmpl (+8/-0)
templates/netrc.tmpl (+10/-0)
templates/secret.tmpl (+5/-0)
templates/wsgi.py.tmpl (+12/-0)
tests/01_deploy.test (+51/-0)
tests/helpers.py (+278/-0)
To merge this branch: bzr merge lp:~patrick-hetu/charms/precise/python-django/python-rewrite
Reviewer Review Type Date Requested Status
Mark Mims (community) Approve
Review via email: mp+171333@code.launchpad.net

Description of the change

This is a huge merge, sorry for that...

I have postponed some work to later like:

* README spellproof: english is not my first language and Bruno is busy
* unused/shared code: I was thinking using charm-helpers in the future

To post a comment you must log in.
Revision history for this message
Jorge Castro (jorge) wrote :

When this lands I'd be more than happy to fix the README

Revision history for this message
Mark Mims (mark-mims) wrote :

ok, great start. Moving to more charm-helpers sounds like a great plan!

review: Approve
82. By Patrick Hetu

add support for mysql database

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file '.bzrignore'
2--- .bzrignore 1970-01-01 00:00:00 +0000
3+++ .bzrignore 2013-06-25 15:22:25 +0000
4@@ -0,0 +1,5 @@
5+*~
6+*.tmp
7+*.py[co]
8+*.sql
9+*.dump
10
11=== renamed file 'README' => 'README.rst'
12--- README 2012-07-06 16:03:46 +0000
13+++ README.rst 2013-06-25 15:22:25 +0000
14@@ -1,48 +1,300 @@
15 Juju charm python-django
16 ========================
17
18-:Author: Patrick Hetu <patrick@koumbit.org>
19-
20-Example deployment
21-------------------
22-
23-1. Setup your wiki specific parameters in mywiki.yaml like this::
24-
25- my_django_site:
26+:Author: Patrick Hetu <patrick.hetu@gmail.com> and Bruno Girin
27+
28+What is Django?
29+...............
30+
31+Django is a high-level web application framework that loosely follows
32+the model-view-controller design pattern. Python's equivalent to Ruby
33+on Rails, Django lets you build complex data-driven websites quickly
34+and easily - Django focuses on automating as much as possible and
35+adhering to the "Don't Repeat Yourself" (DRY) principle. Django
36+additionally emphasizes reusability and "pluggability" of components;
37+many generic third-party "applications" are available to enhance
38+projects or to simply to reduce development time even further.
39+
40+Notable features include:
41+
42+* An object-relational mapper (ORM)
43+* Automatic admin interface
44+* Elegant URL dispatcher
45+* Form serialization and validation system
46+* Templating system
47+* Lightweight, standalone web server for development and testing
48+* Internationalization support
49+* Testing framework and client
50+
51+The charm
52+---------
53+
54+This charm will install Django. It can also install your Django
55+project and his dependencies from either a template or from a
56+version control system.
57+
58+It can also link your project to a database and sync the schemas.
59+This charm also come with a Fabric fabfile to interact with the
60+deployement in a cloud aware manner.
61+
62+
63+Quick start
64+-----------
65+
66+Simply::
67+
68+ juju bootstrap
69+ juju deploy python-django
70+
71+ juju deploy postgresql
72+ juju add-relation python-django postgresql:db
73+
74+ juju deploy gunicorn
75+ juju add-relation python-django gunicorn
76+ juju expose python-django
77+
78+In a couple of minute, your new (vanilla) Django site should be ready at
79+the public address of gunicorn. You can find it in the output of the
80+`juju status` command.
81+
82+This is roughtly equivalent to the `Creating a project`_ step in Django's
83+tutorial.
84+
85+.. _`Creating a project`: https://docs.djangoproject.com/en/1.5/intro/tutorial01/#creating-a-project
86+
87+Example: Deploying using site a template
88+----------------------------------------
89+
90+Setup your Django specific parameters in mydjangosite.yaml like this one::
91+
92+ mydjangosite:
93+ project_template_url: https://github.com/xenith/django-base-template/zipball/master
94+ project_template_extension: py,md,rst
95+
96+Note:
97+
98+ If your using juju-core you must remove the first line
99+ of the file and the indentation for the rest of the file.
100+
101+2. Deployment with `Gunicorn`::
102+
103+ juju bootstrap
104+ juju deploy --config mydjangosite.yaml mydjangosite
105+
106+ juju deploy postgresql
107+ juju add-relation mydjangosite postgresql:db
108+
109+ juju deploy gunicorn
110+ juju add-relation mydjangosite gunicorn
111+ juju expose mydjangosite
112+
113+
114+Example: Deploying using code repository
115+----------------------------------------
116+
117+1. Setup your Django specific parameters in mydjangosite.yaml like this one::
118+
119+ mydjangosite:
120 vcs: bzr
121 repos_url: lp:~patrick-hetu/my_site
122- extra_deb_pkgs: python-dateutils
123- site_domain:
124- site_username:
125- site_password:
126- site_admin_email:
127- site_secret_key:
128- auth_url:
129- api_key:
130- wsgi_worker_class:
131- swift_version:
132- swift_prefix:
133- swift_username:
134- swift_container_name:
135-
136-2. Deployment with Gunicorn::
137+
138+Note:
139+
140+ If your using juju-core you must remove the first line
141+ of the file and the indentation for the rest of the file.
142+
143+2. Deployment with `Gunicorn`::
144
145 juju bootstrap
146- juju deploy --config my_django_site.yaml python-django
147+ juju deploy --config mydjangosite.yaml python-django
148+
149+ juju deploy postgresql
150+ juju add-relation python-django postgresql:db
151+
152 juju deploy gunicorn
153- juju add-relation gunicorn python-django
154- juju expose gunicorn
155-
156-3. Accessing your new moinmoin wiki should be ready at::
157-
158- http://<machine-addr>/
159-
160- To find out the public address of gunicorn, look for it in the output of the
161- `juju status` command.
162+ juju add-relation python-django gunicorn
163+ juju expose python-django
164+
165+Note:
166+
167+ If your using juju-core you must add --upload-tools to the
168+ `juju bootstrap` command.
169+
170+3. Accessing your new Django site should be ready at the public address of
171+ Gunicorn. To find it look for it in the output of the `juju status` command.
172+
173+
174+Project layout and code injection
175+---------------------------------
176+
177+Taking the previous example, your web site should be on the Django node at::
178+
179+ /srv/python-django/
180+
181+As you can see there the charm have inject some code at the end of your settings.py
182+file (or created it if it was not there) to be able to import what's in the
183+`juju_settings/` directory.
184+
185+It's recommended to make your vcs to ignore database and secret files or
186+any files that have information that you don't want to be publish.
187+
188+
189+Upgrade the charm
190+-----------------
191+
192+This charm allow you to upgrade your deployment using the Juju's
193+`upgrade-charm` command. This command will:
194+
195+* upgrade Django
196+* upgrade additionnal pip packages
197+* upgrade additionnal Debian packages
198+* upgrade using requirements files in your project
199+
200+Management with Fabric
201+----------------------
202+
203+Fabric_ is a Python (2.5 or higher) library and command-line tool for
204+streamlining the use of SSH for application deployment or systems
205+administration tasks.
206+
207+It provides a basic suite of operations for executing
208+local or remote shell commands (normally or via sudo) and uploading/downloading
209+files, as well as auxiliary functionality such as prompting the running user
210+for input, or aborting execution.
211+
212+.. _Fabric: http://docs.fabfile.org
213+
214+This charm includes a Fabric script that use Juju's information to perform various
215+tasks.
216+
217+For a list of tasks type this command after bootstraping your Juju environment::
218+
219+ fab -l
220+
221+For example, with a python-django service deployed you can run commands on all its units::
222+
223+ fab -R python-django pull
224+ [10.0.0.2] Executing task 'pull'
225+ [10.0.0.2] run: bzr pull lp:~my_name/django_code/my_site
226+ ...
227+ [10.0.0.2] run: invoke-rc.d gunicorn restart
228+ ...
229+
230+Or you can also run commands on a single unit::
231+
232+ fab -R python-django/0 manage:createsuperuser
233+ ...
234+ [10.0.0.2] out: Username (leave blank to use 'ubuntu'):
235+
236+
237+Limitation:
238+
239+* You can only execute task for one role at the time.
240+ But it can be a service or unit.
241+
242+If you want to extend the fabfile check out fabtools_ .
243+
244+.. _fabtools: http://fabtools.readthedocs.org/
245
246 Security
247 --------
248
249 Note that if your using a *requirement.txt* file the packages will
250-be downloaded with *pip*. *Pip* is not doing any cryptographical
251-verification of its downloads so this be a security risk.
252+be downloaded with *pip* and it doesn't do any cryptographic
253+verification of its downloads.
254+
255+Writing application charm
256+-------------------------
257+
258+To create an application subordinate charm that can be related to this charm you need
259+at least to define an interface named `directory-path` in your `metadate.yaml` file
260+like this::
261+
262+ [...]
263+ requires:
264+ python-django:
265+ interface: directory-path
266+ scope: container
267+ optional: true
268+
269+When you will add a relation between your charm and the python-django charm
270+the hook you will be able to get those relation variables:
271+
272+* settings_dir_path
273+* urls_dir_path
274+* django_admin_cmd
275+* install_root
276+
277+now your charm will be informed about where it need to add new settings
278+and urls files and how to run additionnal Django commands.
279+The Django charm reload Gunicorn after the relation to catch the changes.
280+
281+Changelog
282+---------
283+
284+3:
285+
286+ Notable changes:
287+
288+ * Rewrite the charm using python instead of BASH scripts
289+ * Django projects now need no modification to work with the charm
290+ * Use the `django-admin startproject` command with configurable arguments if no repos is specified
291+ * Juju's generated settings and urls files are now added in a juju_settings
292+ and a juju_urls directories by default
293+ * New MongoDB relation (server side is yet to be done)
294+ * New upgrade hook that upgrade pip and debian packages
295+ * Expose ports is now handle by the charm
296+
297+ Configuration changes:
298+
299+ * default user and group is now ubuntu
300+ * new install_root option
301+ * new django_version option
302+ * new additional_pip_packages option
303+ * new repos_branch,repos_username,repos_password options
304+ * new project_name, project_template_extension, project_template_url options
305+ * new urls_dir_name and settings_dir_name options
306+ * new project_template_url and project_template_extension options
307+ * database, uploads, static, secret and cache settings locations are now configurable
308+ * extra_deb_pkg was renamed additional_distro_packages
309+ * requirements was renamed requirements_pip_files and now support multiple files
310+ * if python_path is empty set as install_root
311+
312+ Backwards incompatible changes:
313+
314+ * swift support was moved to a subordinate charm
315+ * postgresql relation hook was rename pgsql instead of db
316+
317+2:
318+
319+ Notable changes:
320+
321+ * You can configure all wsgi (Gunicorn) settings via the config.yaml file
322+ * Juju compatible Fabric fabfile.py is included for PAAS commands
323+ * Swift storage backend is now optional
324+
325+ Backwards incompatible changes:
326+
327+ * Use splited settings and urls
328+ * Permissons are now based on WSGI's user and group instead of just being www-data
329+ * media and static files are now in new directories ./uploads and ./static/
330+ * Deprecated configuration variables: site_domain, site_username, site_password, site_admin_email
331+
332+
333+1:
334+
335+ Initial release
336+
337+Inspiration
338+-----------
339+
340+* http://www.deploydjango.com
341+* http://lincolnloop.com/django-best-practices/
342+* https://github.com/30loops/djangocms-on-30loops.git
343+* https://github.com/openshift/django-example
344+* http://lincolnloop.com/blog/2013/feb/15/django-settings-parity-youre-doing-it-wrong/
345+* http://tech.yipit.com/2011/11/02/django-settings-what-to-do-about-settings-py/
346+* http://www.rdegges.com/the-perfect-django-settings-file/
347+* https://github.com/xenith/django-base-template.git
348+* https://github.com/transifex/transifex/blob/devel/transifex/settings.py
349+* http://peterlyons.com/problog/2010/02/environment-variables-considered-harmful
350
351=== removed file 'TODO'
352--- TODO 2012-05-09 20:20:08 +0000
353+++ TODO 1970-01-01 00:00:00 +0000
354@@ -1,25 +0,0 @@
355-TODO
356-====
357-
358-* cron/celery
359-* rsyslog
360-* graphite
361-* media_url
362-* extra static path
363-* create bucket + read/write permissions
364-
365-External services
366------------------
367-
368-* Celery / Cron
369-* S3
370-* CloudFiles/Swift
371-* MongoDB
372-* Memcached + PREFIX
373-* Redis
374-
375-
376-BUGS
377-----
378-
379-* virtualenv?
380
381=== modified file 'config.yaml'
382--- config.yaml 2013-01-29 15:33:36 +0000
383+++ config.yaml 2013-06-25 15:22:25 +0000
384@@ -1,102 +1,143 @@
385 options:
386+ site_secret_key:
387+ type: string
388+ default: ''
389+ description: |
390+ The web site secret key. Leave empty will generate one.
391+ NOTE: You **NEED** to set this in a multi-units architecture or you will
392+ have some trouble.
393+ django_version:
394+ type: string
395+ default: "distro"
396+ description: |
397+ Version or origin from which to install. May be one of the following:
398+ distro (default), ppa:somecustom/ppa, a deb url sources entry or
399+ a valid pip line like 'Django' or 'Django==1.5' or a reposiroty url (without the -e).
400 vcs:
401 type: string
402- default: "git"
403+ default: ""
404 description: |
405 The vcs software to use. Only hg, git, bzr, and svn are currently supported.
406 repos_url:
407 type: string
408 default: ""
409 description: The vcs url to checkout.
410+ repos_username:
411+ type: string
412+ default: ""
413+ description: |
414+ The vcs user name.
415+ Note: *Subversion only* settings. For other vcs use the repos_url for auth.
416+ repos_password:
417+ type: string
418+ default: ""
419+ description: |
420+ The vcs password.
421+ Note: *Subversion only* settings. For other vcs use the repos_url for auth.
422 repos_branch:
423 type: string
424 default: ""
425 description: |
426 The repo branch to pull out from. If empty, it will pull out the
427 default branch or trunk (such as origin/master with git).
428- Note that this setting is ignored for bzr and svn as the branch is
429- specified in the URL for both of them.
430- repos_username:
431- type: string
432- default: ""
433- description: The vcs user name.
434- repos_password:
435- type: string
436- default: ""
437- description: The vcs password.
438+ Note that this setting only applies to git. This option is not
439+ supported for hg. For svn and bzr, specify the branch name as
440+ part of the URL.
441+ project_template_url:
442+ type: string
443+ default: ""
444+ description: |
445+ If not repository url is found, the charm will create a new project. This
446+ option is the --template argument value for the startproject command
447+ to use a custom project template.
448+
449+ Django will also accept URLs (http, https, ftp) to compressed
450+ archives with the app template files, downloading and extracting them on the fly.
451+ For more informations see:
452+ https://docs.djangoproject.com/en/dev/ref/django-admin/#startproject-projectname-destination
453+ project_template_extension:
454+ type: string
455+ default: ""
456+ description: |
457+ When Django copies the project template files, it also renders certain
458+ files through the template engine: the files whose extensions match the
459+ --extension option (py by default) and the files whose names are passed with
460+ the --name option.
461+ install_root:
462+ type: string
463+ default: "/srv/"
464+ description: The root directory to checkout to.
465 application_path:
466 type: string
467 default: ""
468 description: |
469- The relative path to the root of the Django application tree where the manage.py
470+ The relative path to install_root where the manage.py
471 script is located.
472- extra_deb_pkgs:
473- type: string
474- default: ""
475- description: Extra Debian packages to install
476- requirements:
477- type: string
478- default: "./requirements.txt"
479- description: |
480- The relative path to the requirement file. Note that the charm
481- won't manually upgrade packages defined in this file.
482- site_domain:
483- type: string
484- default: "example.com"
485- description: The domain name of the web site. This variable can't be changed after installation.
486- site_username:
487- type: string
488- default: "admin"
489- description: The web site administrator username. This variable can't be changed after installation.
490- site_password:
491- type: string
492- default: "changeme"
493- description: The web site administrator password. This variable can't be changed after installation.
494- site_admin_email:
495- type: string
496- default: ""
497- description: The web site administrator email. This variable can't be changed after installation.
498- site_secret_key:
499- type: string
500- default: ''
501- description: |
502- The web site secret key. Leave empty will generate one (Note: you don't
503- want that in a multi-unit architechture).
504- swift_auth_url:
505- type: string
506- default: ""
507- description: Swift authentification url
508- swift_api_key:
509- type: string
510- default: ""
511- description: Swift authentification key
512- swift_version:
513- type: string
514- default: 'v1'
515- description: Swift version
516- swift_prefix:
517- type: string
518- default: 'AUTH_'
519- description: Swift username prefix
520- swift_username:
521- type: string
522- default: ''
523- description: Swift username
524- swift_container_name:
525- type: string
526- default: ''
527- description: Swift container name
528+ additional_distro_packages:
529+ type: string
530+ default: "python-imaging,python-docutils,python-tz"
531+ description: |
532+ Comma separated extra packages to install.
533+ additional_pip_packages:
534+ type: string
535+ default: ""
536+ description: |
537+ Comma separated extra packages to install.
538+ requirements_pip_files:
539+ type: string
540+ default: "requirements.txt,requirements.pip"
541+ description: |
542+ Comma separated relative paths to requirement files. Note that the charm
543+ won't manually upgrade packages defined in this file.
544+ Set the variable to an empty string if you don't want the feature.
545+ requirements_apt_files:
546+ type: string
547+ default: "requirements.apt"
548+ description: |
549+ Comma separated relative paths to requirement files. Note that the charm
550+ won't manually upgrade packages defined in this file.
551+ Set the variable to an empty string if you don't want the feature.
552+ django_settings:
553+ type: string
554+ default: "settings"
555+ description: |
556+ The Python path to your Django settings module.
557+ urls_dir_name:
558+ type: string
559+ default: "juju_urls"
560+ description: |
561+ The place where the generated urls will be written.
562+ Set the variable to an empty string if you don't want the feature.
563+ settings_dir_name:
564+ type: string
565+ default: "juju_settings"
566+ description: |
567+ The place where the generated settings will be written.
568+ Set the variable to an empty string if you don't want the feature.
569+ settings_database_path:
570+ type: string
571+ default: "juju_settings/20-engine-%(engine_name)s.py"
572+ description: |
573+ The place where the database configuration will be appended or written.
574+ Set the variable to an empty string if you don't want the feature.
575+ settings_secret_key_path:
576+ type: string
577+ default: "juju_settings/10-secret.py"
578+ description: |
579+ The place where the secret key configuration will be appended or written.
580+ Set the variable to an empty string if you don't want the feature.
581 wsgi_wsgi_file:
582 type: string
583+ default: "wsgi"
584 description: "The name of the WSGI application."
585 wsgi_workers:
586 type: int
587- default: 2
588- description: "The number of worker process for handling requests."
589+ default: 0
590+ description: "The number of worker process for handling requests. 0 for count(cpu) + 1"
591 wsgi_worker_class:
592 type: string
593 default: "sync"
594- description: "Gunicorn workers type."
595+ description: "Gunicorn workers type. Can be: sync, eventlet, gevent, tornado"
596 wsgi_worker_connections:
597 type: int
598 default: 1000
599@@ -112,11 +153,11 @@
600 wsgi_timeout:
601 type: int
602 default: 30
603- description: ""
604+ description: "Timeout of a request in seconds."
605 wsgi_keep_alive:
606 type: int
607 default: 2
608- description: ""
609+ description: "Keep alive time in seconds."
610 wsgi_umask:
611 type: string
612 default: "0"
613@@ -132,36 +173,36 @@
614 wsgi_log_file:
615 type: string
616 default: "-"
617- description: "The log file to write to. If empty the file would be <your_application_dir>/gunicorn.log"
618+ description: "The log file to write to. If empty the logs would be handle by upstart."
619 wsgi_log_level:
620 type: string
621 default: "info"
622 description: "The granularity of Error log outputs."
623 wsgi_access_logfile:
624 type: string
625- default: "-"
626+ default: ""
627 description: "The Access log file to write to."
628 wsgi_access_logformat:
629 type: string
630- default: '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
631- description: "The Access log format ."
632+ default: ""
633+ description: "The Access log format. Don't forget to escape all quotes and round brackets."
634 wsgi_extra:
635 type: string
636 default: ""
637- description: "Extra settings. For example: {--debug},"
638+ description: "Space separated extra settings. For example: --debug"
639 wsgi_timestamp:
640 type: string
641 default: ""
642- description: "The variable to modify to trigger Gunicorn reload"
643- django_settings:
644- type: string
645- default: ""
646- description: "The Python path to a Django settings module."
647+ description: "The variable to modify to trigger Gunicorn reload."
648 python_path:
649 type: string
650 default: ""
651- description: "Set PYTHONPATH environment variable"
652+ description: "Set an additionnal PYTHONPATH to the project."
653+ listen_ip:
654+ type: string
655+ default: "0.0.0.0"
656+ description: "IP adresses that Gunicorn will listen on. By default we listen on all of them."
657 port:
658 type: int
659 default: 8080
660- description: "Default listen port"
661+ description: "Port the application will be listenning."
662
663=== modified file 'copyright'
664--- copyright 2012-03-27 02:15:31 +0000
665+++ copyright 2013-06-25 15:22:25 +0000
666@@ -1,7 +1,7 @@
667 Format: http://dep.debian.net/deps/dep5/
668
669 Files: *
670-Copyright: Copyright 2011, Patrick Hetu <patrick@koumbit.org>, All Rights Reserved.
671+Copyright: Copyright 2011, Patrick Hetu <patrick.hetu@gmail.com>, All Rights Reserved.
672 License: GPL-3
673 This program is free software: you can redistribute it and/or modify
674 it under the terms of the GNU General Public License as published by
675
676=== added file 'fabfile.py'
677--- fabfile.py 1970-01-01 00:00:00 +0000
678+++ fabfile.py 2013-06-25 15:22:25 +0000
679@@ -0,0 +1,197 @@
680+# Mostly from django-fab-deploy
681+
682+import os
683+import sys
684+from subprocess import Popen, PIPE
685+
686+import yaml
687+
688+from fabric.api import env, run, sudo, task, put
689+from fabric.context_managers import cd
690+from fabric.contrib import files
691+
692+
693+# helpers
694+def _sanitize(s):
695+ s = s.replace(':', '_')
696+ s = s.replace('-', '_')
697+ s = s.replace('/', '_')
698+ s = s.replace('"', '_')
699+ s = s.replace("'", '_')
700+ return s
701+
702+
703+def _config_get(service_name):
704+ yaml_conf = Popen(['juju', 'get', service_name], stdout=PIPE)
705+ conf = yaml.safe_load(yaml_conf.stdout)
706+ orig_conf = yaml.safe_load(open('config.yaml', 'r'))['options']
707+ return {k: (v['value'] if v['value'] is not None else orig_conf[k]['default']) for k,v in conf['settings'].iteritems()}
708+
709+def _find_django_admin_cmd():
710+ for cmd in ['django-admin.py', 'django-admin']:
711+ remote_environ = run('echo $PATH')
712+ for path in remote_environ.split(':'):
713+ path = path.strip('"')
714+ path = os.path.join(path, cmd)
715+ if files.exists(path):
716+ return path
717+
718+# Initialisation
719+env.user = 'ubuntu'
720+
721+d = yaml.safe_load(Popen(['juju','status'],stdout=PIPE).stdout)
722+
723+services = d.get("services", {})
724+if services is None:
725+ sys.exit(0)
726+
727+env.roledefs = {}
728+for service in services.items():
729+ if service is None:
730+ continue
731+
732+ units = services.get(service[0], {}).get("units", {})
733+ if units is None:
734+ continue
735+
736+ for unit in units.items():
737+ if 'public-address' in unit[1].keys():
738+ env.roledefs.setdefault(service[0], []).append(unit[1]['public-address'])
739+ env.roledefs.setdefault(unit[0], []).append(unit[1]['public-address'])
740+
741+
742+env.service_name = env.roles[0].split('/')[0]
743+env.sanitized_service_name = _sanitize(env.service_name)
744+env.conf = _config_get(env.service_name)
745+env.project_dir = os.path.join(env.conf['install_root'], env.sanitized_service_name)
746+env.django_settings_modules = '.'.join([env.sanitized_service_name, env.conf['django_settings']])
747+
748+
749+# Debian
750+@task()
751+def apt_install(packages):
752+ """
753+ Install one or more packages.
754+ """
755+ sudo('apt-get install -y %s' % packages)
756+
757+@task
758+def apt_update():
759+ """
760+ Update APT package definitions.
761+ """
762+ sudo('apt-get update')
763+
764+@task
765+def apt_dist_upgrade():
766+ """
767+ Upgrade all packages.
768+ """
769+ sudo('apt-get dist-upgrade -y')
770+
771+@task
772+def apt_install_r():
773+ """
774+ Install one or more packages listed in your requirements_apt_files.
775+ """
776+ with cd(env.project_dir):
777+ for req_file in env.conf['requirements_apt_files'].split(','):
778+ sudo("apt-get install -y $(cat %s | tr '\\n' ' '" % req_file)
779+
780+# Python
781+@task
782+def pip_install(packages):
783+ """
784+ Install one or more packages.
785+ """
786+ sudo("pip install %s" % packages)
787+
788+@task
789+def pip_install_r():
790+ """
791+ Install one or more python packages listed in your requirements_pip_files.
792+ """
793+ with cd(env.project_dir):
794+ for req_file in env.conf['requirements_pip_files'].split(','):
795+ sudo("pip install -r %s" % req_file)
796+
797+# Users
798+@task
799+def adduser(username):
800+ """
801+ Adduser without password.
802+ """
803+ sudo('adduser %s --disabled-password --gecos ""' % username)
804+
805+@task
806+def ssh_add_key(pub_key_file, username=None):
807+ """
808+ Add a public SSH key to the authorized_keys file on the remote machine.
809+ """
810+ with open(os.path.normpath(pub_key_file), 'rt') as f:
811+ ssh_key = f.read()
812+
813+ if username is None:
814+ run('mkdir -p .ssh')
815+ files.append('.ssh/authorized_keys', ssh_key)
816+ else:
817+ run('mkdir -p /home/%s/.ssh' % username)
818+ files.append('/home/%s/.ssh/authorized_keys' % username, ssh_key)
819+ run('chown -R %s:%s /home/%s/.ssh' % (username, username, username))
820+
821+
822+# VCS
823+
824+@task
825+def pull():
826+ """
827+ pull or update your project code on the remote machine.
828+ """
829+ with cd(env.project_dir):
830+ if env.conf['vcs'] is 'bzr':
831+ run('bzr pull %s' % env.conf['repos_url'])
832+ if env.conf['vcs'] is 'git':
833+ run('git pull %s' % env.conf['repos_url'])
834+ if env.conf['vcs'] is 'hg':
835+ run('hg pull -u %s' % env.conf['repos_url'])
836+ if env.conf['vcs'] is 'svn':
837+ run('svn up %s' % env.conf['repos_url'])
838+
839+ delete_pyc()
840+ reload()
841+
842+
843+# Gunicorn
844+@task
845+def reload():
846+ """
847+ Reload gunicorn.
848+ """
849+ sudo('service %s reload' % env.sanitized_service_name)
850+
851+
852+# Django
853+@task
854+def manage(command):
855+ """ Runs management commands."""
856+ django_admin_cmd = _find_django_admin_cmd()
857+ sudo('%s %s --pythonpath=%s --settings=%s' % \
858+ (django_admin_cmd, command, env.conf['install_root'], env.django_settings_modules), \
859+ user=env.conf['wsgi_user'])
860+
861+@task
862+def load_fixture(fixture_path):
863+ """ Upload and load a fixture file"""
864+ fixture_file = fixture_path.split('/')[-1]
865+ put(fixture_path, '/tmp/')
866+ manage('loaddata %s' % os.path.join('/tmp/', fixture_file))
867+ run('rm %s' % os.path.join('/tmp/', fixture_file))
868+
869+# Utils
870+@task
871+def delete_pyc():
872+ """ Deletes *.pyc files from project source dir """
873+
874+ with env.project_dir:
875+ run("find . -name '*.pyc' -delete")
876+
877
878=== added symlink 'hooks/cache-relation-broken'
879=== target is u'hooks.py'
880=== added symlink 'hooks/cache-relation-changed'
881=== target is u'hooks.py'
882=== added symlink 'hooks/cache-relation-joined'
883=== target is u'hooks.py'
884=== removed file 'hooks/common.incl'
885--- hooks/common.incl 2013-01-28 20:11:56 +0000
886+++ hooks/common.incl 1970-01-01 00:00:00 +0000
887@@ -1,13 +0,0 @@
888-#!/bin/bash
889-
890-UNIT_NAME=$(echo $JUJU_UNIT_NAME | cut -d/ -f1)
891-VCS=$(config-get vcs)
892-REPOS_URL=$(config-get repos_url)
893-UNIT_DIR=/srv/${UNIT_NAME}
894-APP_PATH=$(config-get application_path)
895-if [ -n "$APP_PATH" ]; then
896- APP_DIR=${UNIT_DIR}/${APP_PATH}
897-else
898- APP_DIR=${UNIT_DIR}
899-fi
900-
901
902=== modified file 'hooks/config-changed'
903--- hooks/config-changed 2013-01-28 20:11:56 +0000
904+++ hooks/config-changed 1970-01-01 00:00:00 +0000
905@@ -1,137 +0,0 @@
906-#!/bin/bash
907-set -e
908-
909-#UNIT_NAME=`echo $JUJU_UNIT_NAME | cut -d/ -f1`
910-
911-#VCS=$(config-get vcs)
912-#REPOS_URL=$(config-get repos_url)
913-source $(dirname "$0")/common.incl
914-
915-REQUIREMENTS=$(config-get requirements)
916-
917-SITE_USERNAME=$(config-get site_username)
918-SITE_DOMAIN=$(config-get site_domain)
919-SITE_PASSWORD=$(config-get site_password)
920-SITE_ADMIN_EMAIL=$(config-get site_admin_email)
921-SITE_SECRET_KEY=$(config-get site_secret_key)
922-AUTH_URL=$(config-get auth_url)
923-API_KEY=$(config-get api_key)
924-
925-SWIFT_CONTAINER_NAME=$(config-get swift_container_name)
926-SWIFT_USERNAME=$(config-get swift_username)
927-SWIFT_VERSION=$(config-get swift_version)
928-SWIFT_PREFIX=$(config-get swift_prefix)
929-SWIFT_USERNAME=$(config-get swift_username)
930-
931-name=`basename $0`
932-
933-juju-log "Executing $name"
934-
935-juju-log "django: Repos with $VCS and url: $REPOS_URL"
936-
937-cd /srv/${UNIT_NAME}
938-
939-case $VCS in
940-hg)
941- hg pull ${REPOS_URL}
942- hg update ${REPOS_URL}
943- ;;
944-git)
945- git pull ${REPOS_URL}
946- ;;
947-bzr)
948- bzr pull ${REPOS_URL}
949- ;;
950-svn)
951- svn co ${REPOS_URL}
952- ;;
953-esac
954-
955-cat > /srv/${UNIT_NAME}/initial_data.json << EOF
956-[
957- {
958- "pk": 1,
959- "model": "sites.site",
960- "fields": {
961- "domain": "$SITE_DOMAIN",
962- "name": "$SITE_DOMAIN"
963- }
964- },
965- {
966- "pk": 1,
967- "model": "auth.user",
968- "fields": {
969- "username": "$SITE_USERNAME",
970- "first_name": "",
971- "last_name": "",
972- "is_active": true,
973- "is_superuser": true,
974- "is_staff": true,
975- "last_login": "2010-10-14 23:45:54",
976- "groups": [],
977- "user_permissions": [],
978- "password": "$SITE_PASSWORD",
979- "email": "$SITE_ADMIN_EMAIL",
980- "date_joined": "1990-01-01 00:00"
981- }
982- }
983-]
984-EOF
985-
986-cat > /srv/${UNIT_NAME}/prod_settings.py << EOF
987-# Settings for production server
988-import os
989-
990-PROJECT_ROOT = os.path.dirname(__file__)
991-
992-ADMINS = (
993- ('$SITE_USERNAME', '$SITE_ADMIN_EMAIL'),
994-)
995-
996-MANAGERS = ADMINS
997-
998-DEBUG = False
999-LOCAL = False
1000-SERVE_MEDIA = False
1001-
1002-SEND_BROKEN_LINK_EMAILS = True
1003-
1004-SECRET_KEY = '$SITE_SECRET_KEY'
1005-
1006-MEDIA_ROOT = os.path.join(PROJECT_ROOT, "site_media", "media")
1007-MEDIA_URL = "$AUTH_URL/$SWIFT_VERSION/$SWIFT_PREFIX$SWIFT_USERNAME/$SWIFT_CONTAINER_NAME/"
1008-
1009-STATICFILES_STORAGE = 'cumulus.storage.CloudFilesStorage'
1010-STATICFILES_ROOT = os.path.join(PROJECT_ROOT, "site_media", "static")
1011-STATIC_ROOT = STATICFILES_ROOT
1012-STATICFILES_URL = "$AUTH_URL/$SWIFT_VERSION/$SWIFT_PREFIX$SWIFT_USERNAME/$SWIFT_CONTAINER_NAME/./"
1013-STATIC_URL = STATICFILES_URL
1014-ADMIN_MEDIA_PREFIX = '$AUTH_URL/$SWIFT_VERSION/$SWIFT_PREFIX$SWIFT_USERNAME/$SWIFT_CONTAINER_NAME/admin/'
1015-
1016-DEFAULT_FILE_STORAGE = 'cumulus.storage.CloudFilesStorage'
1017-CUMULUS = {
1018- 'API_KEY': '$API_KEY',
1019- 'AUTH_URL': '$AUTH_URL',
1020- 'CONTAINER': '$SWIFT_CONTAINER_NAME',
1021- 'USE_SSL': False,
1022- 'USERNAME': '$SWIFT_USERNAME',
1023- 'USE_SWIFT_BACKEND' : True,
1024-}
1025-
1026-try:
1027- from db_settings import *
1028-except ImportError:
1029- pass
1030-
1031-EOF
1032-
1033-python ${APP_DIR}/manage.py collectstatic -v 0 --noinput || true
1034-
1035-chown www-data /srv/${UNIT_NAME}/ -R
1036-chmod g+rw /srv/${UNIT_NAME}/ -R
1037-
1038-# Trigger the wsgi server reload
1039-for relid in `relation-ids wsgi` ; do
1040- relation-set -r $relid wsgi_timestamp=`date +%s`
1041-done
1042-
1043
1044=== target is u'hooks.py'
1045=== added symlink 'hooks/django-settings-relation-broken'
1046=== target is u'hooks.py'
1047=== added symlink 'hooks/django-settings-relation-changed'
1048=== target is u'hooks.py'
1049=== added symlink 'hooks/django-settings-relation-joined'
1050=== target is u'hooks.py'
1051=== added file 'hooks/hooks.py'
1052--- hooks/hooks.py 1970-01-01 00:00:00 +0000
1053+++ hooks/hooks.py 2013-06-25 15:22:25 +0000
1054@@ -0,0 +1,854 @@
1055+#!/usr/bin/env python
1056+# vim: et ai ts=4 sw=4:
1057+
1058+import json
1059+import os
1060+import re
1061+import subprocess
1062+import sys
1063+import time
1064+from pwd import getpwnam
1065+from grp import getgrnam
1066+from random import choice
1067+
1068+CHARM_PACKAGES = ["python-pip", "python-jinja2", "mercurial", "git-core",
1069+ "subversion", "bzr", "gettext"]
1070+
1071+INJECTED_WARNING = """
1072+#------------------------------------------------------------------------------
1073+# The following is the import code for the settings directory injected by Juju
1074+#------------------------------------------------------------------------------
1075+"""
1076+
1077+
1078+###############################################################################
1079+# Supporting functions
1080+###############################################################################
1081+MSG_CRITICAL = "CRITICAL"
1082+MSG_DEBUG = "DEBUG"
1083+MSG_INFO = "INFO"
1084+MSG_ERROR = "ERROR"
1085+MSG_WARNING = "WARNING"
1086+
1087+
1088+def juju_log(level, msg):
1089+ subprocess.call(['juju-log', '-l', level, msg])
1090+
1091+#------------------------------------------------------------------------------
1092+# run: Run a command, return the output
1093+#------------------------------------------------------------------------------
1094+def run(command, exit_on_error=True, cwd=None):
1095+ try:
1096+ juju_log(MSG_DEBUG, command)
1097+ return subprocess.check_output(
1098+ command, stderr=subprocess.STDOUT, shell=True, cwd=cwd)
1099+ except subprocess.CalledProcessError, e:
1100+ juju_log(MSG_ERROR, "status=%d, output=%s" % (e.returncode, e.output))
1101+ if exit_on_error:
1102+ sys.exit(e.returncode)
1103+ else:
1104+ raise
1105+
1106+
1107+#------------------------------------------------------------------------------
1108+# install_file: install a file resource. overwites existing files.
1109+#------------------------------------------------------------------------------
1110+def install_file(contents, dest, owner="root", group="root", mode=0600):
1111+ uid = getpwnam(owner)[2]
1112+ gid = getgrnam(group)[2]
1113+ dest_fd = os.open(dest, os.O_WRONLY | os.O_TRUNC | os.O_CREAT, mode)
1114+ os.fchown(dest_fd, uid, gid)
1115+ with os.fdopen(dest_fd, 'w') as destfile:
1116+ destfile.write(str(contents))
1117+
1118+
1119+#------------------------------------------------------------------------------
1120+# install_dir: create a directory
1121+#------------------------------------------------------------------------------
1122+def install_dir(dirname, owner="root", group="root", mode=0700):
1123+ command = \
1124+ '/usr/bin/install -o {} -g {} -m {} -d {}'.format(owner, group, oct(mode),
1125+ dirname)
1126+ return run(command)
1127+
1128+#------------------------------------------------------------------------------
1129+# config_get: Returns a dictionary containing all of the config information
1130+# Optional parameter: scope
1131+# scope: limits the scope of the returned configuration to the
1132+# desired config item.
1133+#------------------------------------------------------------------------------
1134+def config_get(scope=None):
1135+ try:
1136+ config_cmd_line = ['config-get']
1137+ if scope is not None:
1138+ config_cmd_line.append(scope)
1139+ config_cmd_line.append('--format=json')
1140+ config_data = json.loads(subprocess.check_output(config_cmd_line))
1141+ except:
1142+ config_data = None
1143+ finally:
1144+ return(config_data)
1145+
1146+
1147+#------------------------------------------------------------------------------
1148+# relation_json: Returns json-formatted relation data
1149+# Optional parameters: scope, relation_id
1150+# scope: limits the scope of the returned data to the
1151+# desired item.
1152+# unit_name: limits the data ( and optionally the scope )
1153+# to the specified unit
1154+# relation_id: specify relation id for out of context usage.
1155+#------------------------------------------------------------------------------
1156+def relation_json(scope=None, unit_name=None, relation_id=None):
1157+ command = ['relation-get', '--format=json']
1158+ if relation_id is not None:
1159+ command.extend(('-r', relation_id))
1160+ if scope is not None:
1161+ command.append(scope)
1162+ else:
1163+ command.append('-')
1164+ if unit_name is not None:
1165+ command.append(unit_name)
1166+ output = subprocess.check_output(command, stderr=subprocess.STDOUT)
1167+ return output or None
1168+
1169+
1170+#------------------------------------------------------------------------------
1171+# relation_get: Returns a dictionary containing the relation information
1172+# Optional parameters: scope, relation_id
1173+# scope: limits the scope of the returned data to the
1174+# desired item.
1175+# unit_name: limits the data ( and optionally the scope )
1176+# to the specified unit
1177+#------------------------------------------------------------------------------
1178+def relation_get(scope=None, unit_name=None, relation_id=None):
1179+ j = relation_json(scope, unit_name, relation_id)
1180+ if j:
1181+ return json.loads(j)
1182+ else:
1183+ return None
1184+
1185+
1186+def relation_set(keyvalues, relation_id=None):
1187+ args = []
1188+ if relation_id:
1189+ args.extend(['-r', relation_id])
1190+ args.extend(["{}='{}'".format(k, v or '') for k, v in keyvalues.items()])
1191+ run("relation-set {}".format(' '.join(args)))
1192+
1193+ ## Posting json to relation-set doesn't seem to work as documented?
1194+ ## Bug #1116179
1195+ ##
1196+ ## cmd = ['relation-set']
1197+ ## if relation_id:
1198+ ## cmd.extend(['-r', relation_id])
1199+ ## p = Popen(
1200+ ## cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
1201+ ## stderr=subprocess.PIPE)
1202+ ## (out, err) = p.communicate(json.dumps(keyvalues))
1203+ ## if p.returncode:
1204+ ## juju_log(MSG_ERROR, err)
1205+ ## sys.exit(1)
1206+ ## juju_log(MSG_DEBUG, "relation-set {}".format(repr(keyvalues)))
1207+
1208+
1209+def relation_list(relation_id=None):
1210+ """Return the list of units participating in the relation."""
1211+ if relation_id is None:
1212+ relation_id = os.environ['JUJU_RELATION_ID']
1213+ cmd = ['relation-list', '--format=json', '-r', relation_id]
1214+ json_units = subprocess.check_output(cmd).strip()
1215+ if json_units:
1216+ return json.loads(subprocess.check_output(cmd))
1217+ return []
1218+
1219+
1220+#------------------------------------------------------------------------------
1221+# relation_ids: Returns a list of relation ids
1222+# optional parameters: relation_type
1223+# relation_type: return relations only of this type
1224+#------------------------------------------------------------------------------
1225+def relation_ids(relation_types=('db',)):
1226+ # accept strings or iterators
1227+ if isinstance(relation_types, basestring):
1228+ reltypes = [relation_types, ]
1229+ else:
1230+ reltypes = relation_types
1231+ relids = []
1232+ for reltype in reltypes:
1233+ relid_cmd_line = ['relation-ids', '--format=json', reltype]
1234+ json_relids = subprocess.check_output(relid_cmd_line).strip()
1235+ if json_relids:
1236+ relids.extend(json.loads(json_relids))
1237+ return relids
1238+
1239+
1240+#------------------------------------------------------------------------------
1241+# relation_get_all: Returns a dictionary containing the relation information
1242+# optional parameters: relation_type
1243+# relation_type: limits the scope of the returned data to the
1244+# desired item.
1245+#------------------------------------------------------------------------------
1246+def relation_get_all(*args, **kwargs):
1247+ relation_data = []
1248+ relids = relation_ids(*args, **kwargs)
1249+ for relid in relids:
1250+ units_cmd_line = ['relation-list', '--format=json', '-r', relid]
1251+ json_units = subprocess.check_output(units_cmd_line).strip()
1252+ if json_units:
1253+ for unit in json.loads(json_units):
1254+ unit_data = \
1255+ json.loads(relation_json(relation_id=relid,
1256+ unit_name=unit))
1257+ for key in unit_data:
1258+ if key.endswith('-list'):
1259+ unit_data[key] = unit_data[key].split()
1260+ unit_data['relation-id'] = relid
1261+ unit_data['unit'] = unit
1262+ relation_data.append(unit_data)
1263+ return relation_data
1264+
1265+def apt_get_update():
1266+ cmd_line = ['apt-get', 'update']
1267+ return(subprocess.call(cmd_line))
1268+
1269+
1270+#------------------------------------------------------------------------------
1271+# apt_get_install( packages ): Installs package(s)
1272+#------------------------------------------------------------------------------
1273+def apt_get_install(packages=None):
1274+ if packages is None:
1275+ return(False)
1276+ cmd_line = ['apt-get', '-y', 'install', '-qq']
1277+ if isinstance(packages, list):
1278+ cmd_line.extend(packages)
1279+ else:
1280+ cmd_line.append(packages)
1281+ return(subprocess.call(cmd_line))
1282+
1283+
1284+#------------------------------------------------------------------------------
1285+# pip_install( package ): Installs a python package
1286+#------------------------------------------------------------------------------
1287+def pip_install(packages=None, upgrade=False):
1288+ # Build in /tmp or Juju's internal git will be confused
1289+ cmd_line = ['pip', 'install', '-b', '/tmp/']
1290+ if packages is None:
1291+ return(False)
1292+ if upgrade:
1293+ cmd_line.append('--upgrade')
1294+ if not isinstance(packages, list):
1295+ packages = [packages]
1296+
1297+ for package in packages:
1298+ if package.startswith('svn+') or package.startswith('git+') or \
1299+ package.startswith('hg+') or package.startswith('bzr+'):
1300+ cmd_line.append('-e')
1301+ cmd_line.append(package)
1302+
1303+ cmd_line.append('--use-mirrors')
1304+ return(subprocess.call(cmd_line))
1305+
1306+#------------------------------------------------------------------------------
1307+# pip_install_req( path ): Installs a requirements file
1308+#------------------------------------------------------------------------------
1309+def pip_install_req(path=None, upgrade=False):
1310+ # Build in /tmp or Juju's internal git will be confused
1311+ cmd_line = ['pip', 'install', '-b', '/tmp/']
1312+ if path is None:
1313+ return(False)
1314+ if upgrade:
1315+ cmd_line.append('--upgrade')
1316+ cmd_line.append('-r')
1317+ cmd_line.append(path)
1318+ cwd = os.path.dirname(path)
1319+ cmd_line.append('--use-mirrors')
1320+ return(subprocess.call(cmd_line, cwd=cwd))
1321+
1322+#------------------------------------------------------------------------------
1323+# open_port: Convenience function to open a port in juju to
1324+# expose a service
1325+#------------------------------------------------------------------------------
1326+def open_port(port=None, protocol="TCP"):
1327+ if port is None:
1328+ return(None)
1329+ return(subprocess.call(['open-port', "%d/%s" %
1330+ (int(port), protocol)]))
1331+
1332+
1333+#------------------------------------------------------------------------------
1334+# close_port: Convenience function to close a port in juju to
1335+# unexpose a service
1336+#------------------------------------------------------------------------------
1337+def close_port(port=None, protocol="TCP"):
1338+ if port is None:
1339+ return(None)
1340+ return(subprocess.call(['close-port', "%d/%s" %
1341+ (int(port), protocol)]))
1342+
1343+
1344+#------------------------------------------------------------------------------
1345+# update_service_ports: Convenience function that evaluate the old and new
1346+# service ports to decide which ports need to be
1347+# opened and which to close
1348+#------------------------------------------------------------------------------
1349+def update_service_port(old_service_port=None, new_service_port=None):
1350+ if old_service_port is None or new_service_port is None:
1351+ return(None)
1352+ if new_service_port != old_service_port:
1353+ close_port(old_service_port)
1354+ open_port(new_service_port)
1355+
1356+#
1357+# Utils
1358+#
1359+
1360+def install_or_append(contents, dest, owner="root", group="root", mode=0600):
1361+ if os.path.exists(dest):
1362+ uid = getpwnam(owner)[2]
1363+ gid = getgrnam(group)[2]
1364+ dest_fd = os.open(dest, os.O_APPEND, mode)
1365+ os.fchown(dest_fd, uid, gid)
1366+ with os.fdopen(dest_fd, 'a') as destfile:
1367+ destfile.write(str(contents))
1368+ else:
1369+ install_file(contents, dest, owner, group, mode)
1370+
1371+def token_sql_safe(value):
1372+ # Only allow alphanumeric + underscore in database identifiers
1373+ if re.search('[^A-Za-z0-9_]', value):
1374+ return False
1375+ return True
1376+
1377+def sanitize(s):
1378+ s = s.replace(':', '_')
1379+ s = s.replace('-', '_')
1380+ s = s.replace('/', '_')
1381+ s = s.replace('"', '_')
1382+ s = s.replace("'", '_')
1383+ return s
1384+
1385+def user_name(relid, remote_unit, admin=False, schema=False):
1386+ components = [sanitize(relid), sanitize(remote_unit)]
1387+ if admin:
1388+ components.append("admin")
1389+ elif schema:
1390+ components.append("schema")
1391+ return "_".join(components)
1392+
1393+def get_relation_host():
1394+ remote_host = run("relation-get ip")
1395+ if not remote_host:
1396+ # remote unit $JUJU_REMOTE_UNIT uses deprecated 'ip=' component of
1397+ # interface.
1398+ remote_host = run("relation-get private-address")
1399+ return remote_host
1400+
1401+
1402+def get_unit_host():
1403+ this_host = run("unit-get private-address")
1404+ return this_host.strip()
1405+
1406+def process_template(template_name, template_vars, destination):
1407+ # --- exported service configuration file
1408+ from jinja2 import Environment, FileSystemLoader
1409+ template_env = Environment(
1410+ loader=FileSystemLoader(os.path.join(os.environ['CHARM_DIR'],
1411+ 'templates')))
1412+
1413+ template = \
1414+ template_env.get_template(template_name).render(template_vars)
1415+
1416+ with open(destination, 'w') as inject_file:
1417+ inject_file.write(str(template))
1418+
1419+def configure_and_install(rel):
1420+
1421+ def _import_key(id):
1422+ cmd = "apt-key adv --keyserver keyserver.ubuntu.com " \
1423+ "--recv-keys %s" % id
1424+ try:
1425+ subprocess.check_call(cmd.split(' '))
1426+ except:
1427+ juju_log(MSG_ERROR, "Error importing repo key %s" % id)
1428+
1429+ if rel == 'distro':
1430+ return apt_get_install("python-django")
1431+ elif rel[:4] == "ppa:":
1432+ src = rel
1433+ subprocess.check_call(["add-apt-repository", "-y", src])
1434+
1435+ return apt_get_install("python-django")
1436+ elif rel[:3] == "deb":
1437+ l = len(rel.split('|'))
1438+ if l == 2:
1439+ src, key = rel.split('|')
1440+ juju_log("Importing PPA key from keyserver for %s" % src)
1441+ _import_key(key)
1442+ elif l == 1:
1443+ src = rel
1444+ else:
1445+ juju_log(MSG_ERROR, "Invalid django-release: %s" % rel)
1446+
1447+ with open('/etc/apt/sources.list.d/juju_python_django_deb.list', 'w') as f:
1448+ f.write(src)
1449+
1450+ return apt_get_install("python-django")
1451+ elif rel == '':
1452+ return pip_install('Django')
1453+ else:
1454+ return pip_install(rel)
1455+
1456+#
1457+# from:
1458+# http://stackoverflow.com/questions/377017/test-if-executable-exists-in-python
1459+#
1460+def which(program):
1461+ def is_exe(fpath):
1462+ return os.path.isfile(fpath) and os.access(fpath, os.X_OK)
1463+
1464+ for path in os.environ["PATH"].split(os.pathsep):
1465+ path = path.strip('"')
1466+ exe_file = os.path.join(path, program)
1467+ if is_exe(exe_file):
1468+ return exe_file
1469+
1470+ return False
1471+
1472+def find_django_admin_cmd():
1473+ for cmd in ['django-admin.py', 'django-admin']:
1474+ django_admin_cmd = which(cmd)
1475+ if django_admin_cmd:
1476+ return django_admin_cmd
1477+
1478+ juju_log(MSG_ERROR, "No django-admin executable found.")
1479+
1480+def append_template(template_name, template_vars, path, try_append=False):
1481+
1482+ # --- exported service configuration file
1483+ from jinja2 import Environment, FileSystemLoader
1484+ template_env = Environment(
1485+ loader=FileSystemLoader(os.path.join(os.environ['CHARM_DIR'],
1486+ 'templates')))
1487+
1488+ template = \
1489+ template_env.get_template(template_name).render(template_vars)
1490+
1491+ append = False
1492+ if os.path.exists(path):
1493+ with open(path, 'r') as inject_file:
1494+ if not str(template) in inject_file:
1495+ append = True
1496+ else:
1497+ append = True
1498+
1499+ if append == True:
1500+ with open(path, 'a') as inject_file:
1501+ inject_file.write(INJECTED_WARNING)
1502+ inject_file.write(str(template))
1503+
1504+
1505+
1506+###############################################################################
1507+# Hook functions
1508+###############################################################################
1509+def install():
1510+
1511+ for retry in xrange(0,24):
1512+ if apt_get_install(CHARM_PACKAGES):
1513+ time.sleep(10)
1514+ else:
1515+ break
1516+
1517+ configure_and_install(django_version)
1518+
1519+ django_admin_cmd = find_django_admin_cmd()
1520+
1521+ if extra_deb_pkgs:
1522+ apt_get_install(extra_deb_pkgs.split(','))
1523+
1524+ if extra_pip_pkgs:
1525+ for package in extra_pip_pkgs.split(','):
1526+ pip_install(package)
1527+
1528+ if repos_username:
1529+ m = re.match(".*://([^/]+)/.*", repos_url)
1530+ if m is not None:
1531+ repos_domain = m.group(1)
1532+ template_vars = {
1533+ 'repos_domain': repos_domain,
1534+ 'repos_username': repos_username,
1535+ 'repos_password': repos_password
1536+ }
1537+ from os.path import expanduser
1538+ process_template('netrc.tmpl', template_vars, expanduser('~/.netrc'))
1539+ else:
1540+ juju_log(MSG_ERROR, '''Failed to process repos_username and repos_password:\n
1541+ cannot identify domain in URL {0}'''.format(repos_url))
1542+
1543+ if vcs == 'hg' or vcs == 'mercurial':
1544+ run('hg clone %s %s' % (repos_url, vcs_clone_dir))
1545+ elif vcs == 'git' or vcs == 'git-core':
1546+ if repos_branch:
1547+ run('git clone %s -b %s %s' % (repos_url, repos_branch, vcs_clone_dir))
1548+ else:
1549+ run('git clone %s %s' % (repos_url, vcs_clone_dir))
1550+ elif vcs == 'bzr' or vcs == 'bazaar':
1551+ run('bzr branch %s %s' % (repos_url, vcs_clone_dir))
1552+ elif vcs == 'svn' or vcs == 'subversion':
1553+ run('svn co %s %s' % (repos_url, vcs_clone_dir))
1554+ elif vcs == '' and repos_url == '':
1555+ juju_log(MSG_INFO, "No version control using django-admin startproject")
1556+ cmd = '%s startproject' % django_admin_cmd
1557+ if project_template_url:
1558+ cmd = " ".join([cmd, '--template', project_template_url])
1559+ if project_template_extension:
1560+ cmd = " ".join([cmd, '--extension', project_template_extension])
1561+ try:
1562+ run('%s %s %s' % (cmd, sanitized_unit_name, install_root), exit_on_error=False)
1563+ except subprocess.CalledProcessError:
1564+ run('%s %s' % (cmd, sanitized_unit_name), cwd=install_root)
1565+
1566+ else:
1567+ juju_log(MSG_ERROR, "Unknown version control")
1568+ sys.exit(1)
1569+
1570+ run('chown -R %s:%s %s' % (wsgi_user,wsgi_group, working_dir))
1571+
1572+ install_dir(settings_dir_path, owner=wsgi_user, group=wsgi_group, mode=0755)
1573+ install_dir(urls_dir_path, owner=wsgi_user, group=wsgi_group, mode=0755)
1574+
1575+ #FIXME: Upgrades/pulls will mess those files
1576+
1577+ for path, dir in ((settings_py_path, 'juju_settings'), (urls_py_path, 'juju_urls')):
1578+ append_template('conf_injection.tmpl', {'dir': dir}, path)
1579+
1580+ if requirements_pip_files:
1581+ for req_file in requirements_pip_files.split(','):
1582+ pip_install_req(os.path.join(working_dir,req_file))
1583+
1584+ wsgi_py_path = os.path.join(working_dir, 'wsgi.py')
1585+ if not os.path.exists(wsgi_py_path):
1586+ process_template('wsgi.py.tmpl', {'project_name': sanitized_unit_name, \
1587+ 'django_settings': django_settings}, \
1588+ wsgi_py_path)
1589+
1590+def start():
1591+ if os.path.exists(os.path.join('/etc/init/', sanitized_unit_name + '.conf')):
1592+ run("service %s restart || service %s start" % (sanitized_unit_name, sanitized_unit_name))
1593+
1594+def stop():
1595+ if os.path.exists(os.path.join('/etc/init/', sanitized_unit_name + '.conf')):
1596+ run('service %s stop' % sanitized_unit_name)
1597+
1598+def config_changed(config_data):
1599+ os.environ['DJANGO_SETTINGS_MODULE'] = django_settings_modules
1600+ django_admin_cmd = find_django_admin_cmd()
1601+
1602+ site_secret_key = config_data['site_secret_key']
1603+ if not site_secret_key:
1604+ site_secret_key = ''.join([choice('abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)') for i in range(50)])
1605+
1606+ process_template('secret.tmpl', {'site_secret_key': site_secret_key}, settings_secret_path)
1607+
1608+ # Trigger WSGI reloading
1609+ for relid in relation_ids('wsgi'):
1610+ relation_set({'wsgi_timestamp': time.time()}, relation_id=relid)
1611+
1612+def upgrade():
1613+ if extra_pip_pkgs:
1614+ for package in extra_pip_pkgs.split(','):
1615+ pip_install(package, upgrade=True)
1616+
1617+ apt_get_update()
1618+ for retry in xrange(0,24):
1619+ if apt_get_install(CHARM_PACKAGES):
1620+ time.sleep(10)
1621+ else:
1622+ break
1623+
1624+ if vcs == 'hg' or vcs == 'mercurial':
1625+ run('hg pull %s %s' % (repos_url, vcs_clone_dir))
1626+ elif vcs == 'git' or vcs == 'git-core':
1627+ if repos_branch:
1628+ run('git pull %s -b %s %s' % (repos_url, repos_branch, vcs_clone_dir))
1629+ else:
1630+ run('git pull %s %s' % (repos_url, vcs_clone_dir))
1631+ elif vcs == 'bzr' or vcs == 'bazaar':
1632+ run('bzr pull %s %s' % (repos_url, vcs_clone_dir))
1633+ elif vcs == 'svn' or vcs == 'subversion':
1634+ run('svn up %s %s' % (repos_url, vcs_clone_dir))
1635+ else:
1636+ juju_log(MSG_ERROR, "Unknown version control")
1637+ sys.exit(1)
1638+
1639+ run('chown -R %s:%s %s' % (wsgi_user,wsgi_group, working_dir))
1640+
1641+ if requirements_pip_files:
1642+ for req_file in requirements_pip_files.split(','):
1643+ pip_install_req(os.path.join(working_dir,req_file), upgrade=True)
1644+
1645+
1646+ # Trigger WSGI reloading
1647+ for relid in relation_ids('wsgi'):
1648+ relation_set({'wsgi_timestamp': time.time()}, relation_id=relid)
1649+
1650+ for relid in relation_ids('django-settings'):
1651+ relation_set({'django_settings_timestamp': time.time()}, relation_id=relid)
1652+
1653+
1654+def django_settings_relation_joined_changed():
1655+ os.environ['DJANGO_SETTINGS_MODULE'] = '.'.join([sanitized_unit_name, 'settings'])
1656+ django_admin_cmd = find_django_admin_cmd()
1657+
1658+ relation_set({'settings_dir_path': settings_dir_path,
1659+ 'urls_dir_path': urls_dir_path,
1660+ 'install_root': install_root,
1661+ 'django_admin_cmd': django_admin_cmd,
1662+ 'wsgi_user': wsgi_user,
1663+ 'wsgi_group': wsgi_group,
1664+ })
1665+
1666+ run('chown -R %s:%s %s' % (wsgi_user,wsgi_group, working_dir))
1667+
1668+ # Trigger WSGI reloading
1669+ for relid in relation_ids('wsgi'):
1670+ relation_set({'wsgi_timestamp': time.time()}, relation_id=relid)
1671+
1672+def django_settings_relation_broken():
1673+ pass
1674+
1675+def pgsql_relation_joined_changed():
1676+ os.environ['DJANGO_SETTINGS_MODULE'] = '.'.join([sanitized_unit_name, 'settings'])
1677+ django_admin_cmd = find_django_admin_cmd()
1678+
1679+ packages = ["python-psycopg2", "postgresql-client"]
1680+ apt_get_install(packages)
1681+
1682+ database = relation_get("database")
1683+ if not database:
1684+ return
1685+
1686+ templ_vars = {
1687+ 'db_engine': 'django.db.backends.postgresql_psycopg2',
1688+ 'db_database': database,
1689+ 'db_user': relation_get("user"),
1690+ 'db_password': relation_get("password"),
1691+ 'db_host': relation_get("host"),
1692+ }
1693+
1694+ process_template('engine.tmpl', templ_vars, settings_database_path % {'engine_name': 'pgsql'})
1695+
1696+ run("%s syncdb --noinput --pythonpath=%s --settings=%s || true" % \
1697+ (django_admin_cmd, install_root, django_settings_modules))
1698+
1699+
1700+ run('chown -R %s:%s %s' % (wsgi_user,wsgi_group, working_dir))
1701+
1702+ # Trigger WSGI reloading
1703+ for relid in relation_ids('wsgi'):
1704+ relation_set({'wsgi_timestamp': time.time()}, relation_id=relid)
1705+
1706+def pgsql_relation_broken():
1707+ run('rm %s' % settings_database_path % {'engine_name': 'pgsql'})
1708+
1709+ # Trigger WSGI reloading
1710+ for relid in relation_ids('wsgi'):
1711+ relation_set({'wsgi_timestamp': time.time()}, relation_id=relid)
1712+
1713+def mongodb_relation_joined_changed():
1714+ packages = ["python-mongoengine"]
1715+ apt_get_install(packages)
1716+
1717+ database = relation_get("database")
1718+ if not database:
1719+ return
1720+
1721+ templ_vars = {
1722+ 'db_database': database,
1723+ 'db_host': relation_get("host"),
1724+ }
1725+
1726+ process_template('mongodb_engine.tmpl', templ_vars, settings_database_path % {'engine_name': 'mongodb'})
1727+
1728+ run('chown -R %s:%s %s' % (wsgi_user,wsgi_group, working_dir))
1729+
1730+ # Trigger WSGI reloading
1731+ for relid in relation_ids('wsgi'):
1732+ relation_set({'wsgi_timestamp': time.time()}, relation_id=relid)
1733+
1734+def mongodb_relation_broken():
1735+ run('rm %s' % settings_database_path % {'engine_name': 'mongodb'})
1736+
1737+ # Trigger WSGI reloading
1738+ for relid in relation_ids('wsgi'):
1739+ relation_set({'wsgi_timestamp': time.time()}, relation_id=relid)
1740+
1741+def wsgi_relation_joined_changed():
1742+ relation_set({'working_dir':working_dir})
1743+
1744+ for var in config_data:
1745+ if var.startswith('wsgi_') or var in ['listen_ip', 'port']:
1746+ relation_set({var: config_data[var]})
1747+
1748+ if not config_data['python_path']:
1749+ relation_set({'python_path': install_root})
1750+
1751+ open_port(config_data['port'])
1752+
1753+def wsgi_relation_broken():
1754+ close_port(config_data['port'])
1755+
1756+def cache_relation_joined_changed():
1757+ os.environ['DJANGO_SETTINGS_MODULE'] = django_settings_modules
1758+
1759+ packages = ["python-memcache"]
1760+ apt_get_install(packages)
1761+
1762+ host = relation_get("host")
1763+ if not host:
1764+ return
1765+
1766+ templ_vars = {
1767+ 'cache_engine': 'django.core.cache.backends.memcached.MemcachedCache',
1768+ 'cache_host': relation_get("host"),
1769+ 'cache_port': relation_get("port"),
1770+ }
1771+
1772+ process_template('cache.tmpl', templ_vars, settings_database_path % {'engine_name': 'memcache'})
1773+
1774+ run('chown -R %s:%s %s' % (wsgi_user,wsgi_group, working_dir))
1775+
1776+
1777+ # Trigger WSGI reloading
1778+ for relid in relation_ids('wsgi'):
1779+ relation_set({'wsgi_timestamp': time.time()}, relation_id=relid)
1780+
1781+def cache_relation_broken():
1782+ run('rm %s' % settings_database_path % {'engine_name': 'memcache'})
1783+
1784+ # Trigger WSGI reloading
1785+ for relid in relation_ids('wsgi'):
1786+ relation_set({'wsgi_timestamp': time.time()}, relation_id=relid)
1787+
1788+def website_relation_joined_changed():
1789+ relation_set({'port': config_data["port"], 'hostname': get_unit_host()})
1790+
1791+def website_relation_broken():
1792+ pass
1793+
1794+###############################################################################
1795+# Global variables
1796+###############################################################################
1797+config_data = config_get()
1798+juju_log(MSG_DEBUG, "got config: %s" % str(config_data))
1799+
1800+django_version = config_data['django_version']
1801+vcs = config_data['vcs']
1802+repos_url = config_data['repos_url']
1803+repos_username = config_data['repos_username']
1804+repos_password = config_data['repos_password']
1805+repos_branch = config_data['repos_branch']
1806+
1807+project_template_extension = config_data['project_template_extension']
1808+project_template_url = config_data['project_template_url']
1809+
1810+extra_deb_pkgs = config_data['additional_distro_packages']
1811+extra_pip_pkgs = config_data['additional_pip_packages']
1812+requirements_pip_files = config_data['requirements_pip_files']
1813+wsgi_user = config_data['wsgi_user']
1814+wsgi_group = config_data['wsgi_group']
1815+install_root = config_data['install_root']
1816+application_path = config_data['application_path']
1817+django_settings = config_data['django_settings']
1818+
1819+unit_name = os.environ['JUJU_UNIT_NAME'].split('/')[0]
1820+sanitized_unit_name = sanitize(unit_name)
1821+vcs_clone_dir = os.path.join(install_root, sanitized_unit_name)
1822+if application_path:
1823+ working_dir = os.path.join(vcs_clone_dir, application_path)
1824+else:
1825+ working_dir = vcs_clone_dir
1826+
1827+django_settings_modules = '.'.join([sanitized_unit_name, django_settings])
1828+django_run_dir = os.path.join(working_dir, "run/")
1829+django_logs_dir = os.path.join(working_dir, "logs/")
1830+settings_py_path = os.path.join(working_dir, 'settings.py')
1831+urls_py_path = os.path.join(working_dir, 'urls.py')
1832+settings_dir_path = os.path.join(working_dir, config_data["settings_dir_name"])
1833+urls_dir_path = os.path.join(working_dir, config_data["urls_dir_name"])
1834+settings_secret_path = os.path.join(working_dir, config_data["settings_secret_key_path"])
1835+settings_database_path = os.path.join(working_dir, config_data["settings_database_path"])
1836+hook_name = os.path.basename(sys.argv[0])
1837+
1838+###############################################################################
1839+# Main section
1840+###############################################################################
1841+def main():
1842+ juju_log(MSG_INFO, "Running {} hook".format(hook_name))
1843+ if hook_name == "install":
1844+ install()
1845+
1846+ elif hook_name == "start":
1847+ start()
1848+
1849+ elif hook_name == "stop":
1850+ stop()
1851+
1852+ elif hook_name == "config-changed":
1853+ config_changed(config_data)
1854+
1855+ elif hook_name == "upgrade-charm":
1856+ upgrade()
1857+ config_changed(config_data)
1858+
1859+ elif hook_name in ["django-settings-relation-joined", "django-settings-relation-changed"]:
1860+ django_settings_relation_joined_changed()
1861+ config_changed(config_data)
1862+
1863+ elif hook_name == "django-settings-relation-broken":
1864+ django_settings_relation_broken()
1865+ config_changed(config_data)
1866+
1867+ elif hook_name in ["pgsql-relation-joined", "pgsql-relation-changed"]:
1868+ pgsql_relation_joined_changed()
1869+ config_changed(config_data)
1870+
1871+ elif hook_name == "pgsql-relation-broken":
1872+ pgsql_relation_broken()
1873+ config_changed(config_data)
1874+
1875+ elif hook_name in ["mongodb-relation-joined", "mongodb-relation-changed"]:
1876+ mongodb_relation_joined_changed()
1877+ config_changed(config_data)
1878+
1879+ elif hook_name == "mongodb-relation-broken":
1880+ mongodb_relation_broken()
1881+ config_changed(config_data)
1882+
1883+ elif hook_name in ["wsgi-relation-joined", "wsgi-relation-changed"]:
1884+ wsgi_relation_joined_changed()
1885+
1886+ elif hook_name == "wsgi-relation-broken":
1887+ wsgi_relation_broken()
1888+
1889+ elif hook_name in ["cache-relation-joined", "cache-relation-changed"]:
1890+ cache_relation_joined_changed()
1891+
1892+ elif hook_name == "cache-relation-broken":
1893+ cache_relation_broken()
1894+
1895+ elif hook_name in ["website-relation-joined", "website-relation-changed"]:
1896+ website_relation_joined_changed()
1897+
1898+ elif hook_name == "website-relation-broken":
1899+ website_relation_broken()
1900+
1901+
1902+ else:
1903+ print "Unknown hook {}".format(hook_name)
1904+ raise SystemExit(1)
1905+
1906+
1907+if __name__ == '__main__':
1908+ raise SystemExit(main())
1909
1910=== modified file 'hooks/install'
1911--- hooks/install 2013-01-29 15:33:36 +0000
1912+++ hooks/install 1970-01-01 00:00:00 +0000
1913@@ -1,61 +0,0 @@
1914-#!/bin/bash
1915-set -e
1916-
1917-source $(dirname "$0")/common.incl
1918-
1919-REPOS_URL=$(config-get repos_url)
1920-REPOS_BRANCH=$(config-get repos_branch)
1921-REPOS_USERNAME=$(config-get repos_username)
1922-REPOS_PASSWORD=$(config-get repos_password)
1923-EXTRA_DEB_PKGS=$(config-get extra_deb_pkgs)
1924-REQUIREMENTS=$(config-get requirements)
1925-
1926-
1927-juju-log "django: Repos with $VCS and url: $REPOS_URL"
1928-
1929-if [ -n "$REPOS_USERNAME" ]; then
1930- if [[ "$REPOS_URL" =~ .*://([^/]+)/.* ]]; then
1931- REPOS_DOMAIN=${BASH_REMATCH[1]}
1932- cat >> ~/.netrc << EOF
1933-machine ${REPOS_DOMAIN}
1934- login ${REPOS_USERNAME}
1935- password ${REPOS_PASSWORD}
1936-
1937-EOF
1938- chmod 600 ~/.netrc
1939- else
1940- juju-log "Cannot retrieve domain name from URL $REPOS_URL"
1941- fi
1942-fi
1943-
1944-apt-get install -y python-django python-imaging python-docutils python-psycopg2 python-pip python-jinja2 mercurial git-core subversion bzr postgresql-client $EXTRA_DEB_PKGS
1945-
1946-
1947-if [ ! -d "${UNIT_DIR}" ]; then
1948-
1949- case $VCS in
1950- hg)
1951- hg clone ${REPOS_URL} ${UNIT_DIR}
1952- ;;
1953- git)
1954- if [ -n "$REPOS_BRANCH" ]; then
1955- git clone ${REPOS_URL} -b ${REPOS_BRANCH} ${UNIT_DIR}
1956- else
1957- git clone ${REPOS_URL} ${UNIT_DIR}
1958- fi
1959- ;;
1960- bzr)
1961- bzr branch ${REPOS_URL} ${UNIT_DIR}
1962- ;;
1963- svn)
1964- svn co ${REPOS_URL} ${UNIT_DIR}
1965- ;;
1966- esac
1967-fi
1968-
1969-mkdir -p ${UNIT_DIR}/run
1970-mkdir -p ${UNIT_DIR}/logs
1971-
1972-cd ${UNIT_DIR}
1973-pip install -r ${REQUIREMENTS} || true
1974-
1975
1976=== target is u'hooks.py'
1977=== added symlink 'hooks/mongodb-relation-broken'
1978=== target is u'hooks.py'
1979=== added symlink 'hooks/mongodb-relation-changed'
1980=== target is u'hooks.py'
1981=== added symlink 'hooks/mongodb-relation-joined'
1982=== target is u'hooks.py'
1983=== added symlink 'hooks/pgsql-relation-broken'
1984=== target is u'hooks.py'
1985=== renamed file 'hooks/db-relation-changed' => 'hooks/pgsql-relation-changed'
1986--- hooks/db-relation-changed 2013-01-28 20:11:56 +0000
1987+++ hooks/pgsql-relation-changed 1970-01-01 00:00:00 +0000
1988@@ -1,51 +0,0 @@
1989-#!/bin/bash
1990-set -e
1991-
1992-source $(dirname "$0")/common.incl
1993-
1994-relation-set private-address=`unit-get private-address`
1995-
1996-base=`dirname $0`
1997-
1998-#UNIT_NAME=`echo $JUJU_UNIT_NAME | cut -d/ -f1`
1999-#UNIT_DIR=/srv/${UNIT_NAME}
2000-
2001-DB_USER=$(relation-get user)
2002-DB_PASSWORD=$(relation-get password)
2003-DB_HOST=$(relation-get host)
2004-DB_DATABASE=$(relation-get database)
2005-
2006-name=`basename $0`
2007-
2008-juju-log "Executing $name"
2009-
2010-if [ -z "$DB_USER" ] ; then
2011- juju-log "No database information yet."
2012- exit 0 # wait for future handshake from database service unit
2013-fi
2014-
2015-cat > ${APP_DIR}/db_settings.py << EOF
2016-# Settings for database connexion
2017-
2018-DATABASES = {
2019- "default": {
2020- "ENGINE": 'django.db.backends.postgresql_psycopg2',
2021- "NAME": '$DB_DATABASE',
2022- "USER": '$DB_USER',
2023- "PASSWORD": '$DB_PASSWORD',
2024- "HOST": '$DB_HOST',
2025- "PORT": '',
2026- }
2027-}
2028-
2029-EOF
2030-
2031-
2032-python ${APP_DIR}/manage.py syncdb --noinput
2033-python ${APP_DIR}/manage.py migrate --noinput
2034-
2035-chown www-data ${UNIT_DIR} -R
2036-chmod g+rw ${UNIT_DIR} -R
2037-
2038-exec $base/config-changed
2039-
2040
2041=== target is u'hooks.py'
2042=== renamed symlink 'hooks/db-relation-joined' => 'hooks/pgsql-relation-joined'
2043=== target changed u'db-relation-changed' => u'hooks.py'
2044=== added symlink 'hooks/start'
2045=== target is u'hooks.py'
2046=== added symlink 'hooks/stop'
2047=== target is u'hooks.py'
2048=== modified file 'hooks/upgrade-charm'
2049--- hooks/upgrade-charm 2012-07-07 14:38:26 +0000
2050+++ hooks/upgrade-charm 1970-01-01 00:00:00 +0000
2051@@ -1,11 +0,0 @@
2052-#!/bin/sh
2053-set -e
2054-
2055-home=`dirname $0`
2056-
2057-juju-log "Upgrading charm by running install hook again."
2058-$home/install
2059-
2060-juju-log "Upgrading charm, running config-changed hook again."
2061-$home/config-changed
2062-
2063
2064=== target is u'hooks.py'
2065=== added symlink 'hooks/website-relation-broken'
2066=== target is u'hooks.py'
2067=== modified symlink 'hooks/website-relation-changed'
2068=== target changed u'website-relation-joined' => u'hooks.py'
2069=== modified file 'hooks/website-relation-joined'
2070--- hooks/website-relation-joined 2012-07-09 16:00:51 +0000
2071+++ hooks/website-relation-joined 1970-01-01 00:00:00 +0000
2072@@ -1,14 +0,0 @@
2073-#!/bin/bash
2074-set -e
2075-
2076-unit_name=${JUJU_UNIT_NAME//\//-}
2077-
2078-if [ -e /etc/gunicorn.d/${unit_name}.conf ]; then
2079-
2080- bind_line=$(grep "bind=0.0.0.0:" /etc/gunicorn.d/${unit_name}.conf)
2081- PORT=$(echo ${bind_line} | grep -o ":[0-9]*" | sed -e "s/://")
2082-
2083- juju-log "PORT=${PORT}"
2084-
2085- relation-set port="${PORT}" hostname=`unit-get private-address`
2086-fi
2087
2088=== target is u'hooks.py'
2089=== added symlink 'hooks/wsgi-relation-broken'
2090=== target is u'hooks.py'
2091=== modified symlink 'hooks/wsgi-relation-changed'
2092=== target changed u'wsgi-relation-joined' => u'hooks.py'
2093=== modified file 'hooks/wsgi-relation-joined'
2094--- hooks/wsgi-relation-joined 2013-01-24 19:50:26 +0000
2095+++ hooks/wsgi-relation-joined 1970-01-01 00:00:00 +0000
2096@@ -1,19 +0,0 @@
2097-#!/bin/bash
2098-set -e
2099-
2100-UNIT_NAME=`echo $JUJU_UNIT_NAME | cut -d/ -f1`
2101-
2102-relation-set working_dir="/srv/${UNIT_NAME}/"
2103-
2104-variables="wsgi_wsgi_file wsgi_workers wsgi_worker_class wsgi_worker_connections wsgi_max_requests wsgi_timeout wsgi_backlog wsgi_keep_alive wsgi_extra wsgi_user wsgi_group wsgi_umask wsgi_log_file wsgi_log_level wsgi_access_logfile wsgi_access_logformat env_extra django_settings python_path port"
2105-
2106-declare -A VAR
2107-for v in $variables;do
2108- VAR[$v]=$(config-get $v)
2109- if [ ! -z "${VAR[$v]}" ] ; then
2110- relation-set "$v=${VAR[$v]}"
2111- fi
2112-done
2113-
2114-juju-log "Set relation variables: ${VAR[@]}"
2115-
2116
2117=== target is u'hooks.py'
2118=== added file 'icon.svg'
2119--- icon.svg 1970-01-01 00:00:00 +0000
2120+++ icon.svg 2013-06-25 15:22:25 +0000
2121@@ -0,0 +1,395 @@
2122+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2123+<!-- Created with Inkscape (http://www.inkscape.org/) -->
2124+
2125+<svg
2126+ xmlns:dc="http://purl.org/dc/elements/1.1/"
2127+ xmlns:cc="http://creativecommons.org/ns#"
2128+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
2129+ xmlns:svg="http://www.w3.org/2000/svg"
2130+ xmlns="http://www.w3.org/2000/svg"
2131+ xmlns:xlink="http://www.w3.org/1999/xlink"
2132+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
2133+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
2134+ width="96"
2135+ height="96"
2136+ id="svg6517"
2137+ version="1.1"
2138+ inkscape:version="0.48.4 r9939"
2139+ sodipodi:docname="icon.svg">
2140+ <defs
2141+ id="defs6519">
2142+ <linearGradient
2143+ inkscape:collect="always"
2144+ xlink:href="#Background"
2145+ id="linearGradient6461"
2146+ gradientUnits="userSpaceOnUse"
2147+ x1="0"
2148+ y1="970.29498"
2149+ x2="144"
2150+ y2="970.29498"
2151+ gradientTransform="matrix(0,-0.66666669,0.6660448,0,-866.25992,731.29077)" />
2152+ <linearGradient
2153+ id="Background">
2154+ <stop
2155+ id="stop4178"
2156+ offset="0"
2157+ style="stop-color:#574c4a;stop-opacity:1" />
2158+ <stop
2159+ id="stop4180"
2160+ offset="1"
2161+ style="stop-color:#80716d;stop-opacity:1" />
2162+ </linearGradient>
2163+ <filter
2164+ style="color-interpolation-filters:sRGB;"
2165+ inkscape:label="Inner Shadow"
2166+ id="filter1121">
2167+ <feFlood
2168+ flood-opacity="0.59999999999999998"
2169+ flood-color="rgb(0,0,0)"
2170+ result="flood"
2171+ id="feFlood1123" />
2172+ <feComposite
2173+ in="flood"
2174+ in2="SourceGraphic"
2175+ operator="out"
2176+ result="composite1"
2177+ id="feComposite1125" />
2178+ <feGaussianBlur
2179+ in="composite1"
2180+ stdDeviation="1"
2181+ result="blur"
2182+ id="feGaussianBlur1127" />
2183+ <feOffset
2184+ dx="0"
2185+ dy="2"
2186+ result="offset"
2187+ id="feOffset1129" />
2188+ <feComposite
2189+ in="offset"
2190+ in2="SourceGraphic"
2191+ operator="atop"
2192+ result="composite2"
2193+ id="feComposite1131" />
2194+ </filter>
2195+ <filter
2196+ style="color-interpolation-filters:sRGB;"
2197+ inkscape:label="Drop Shadow"
2198+ id="filter950">
2199+ <feFlood
2200+ flood-opacity="0.25"
2201+ flood-color="rgb(0,0,0)"
2202+ result="flood"
2203+ id="feFlood952" />
2204+ <feComposite
2205+ in="flood"
2206+ in2="SourceGraphic"
2207+ operator="in"
2208+ result="composite1"
2209+ id="feComposite954" />
2210+ <feGaussianBlur
2211+ in="composite1"
2212+ stdDeviation="1"
2213+ result="blur"
2214+ id="feGaussianBlur956" />
2215+ <feOffset
2216+ dx="0"
2217+ dy="1"
2218+ result="offset"
2219+ id="feOffset958" />
2220+ <feComposite
2221+ in="SourceGraphic"
2222+ in2="offset"
2223+ operator="over"
2224+ result="composite2"
2225+ id="feComposite960" />
2226+ </filter>
2227+ <clipPath
2228+ clipPathUnits="userSpaceOnUse"
2229+ id="clipPath873">
2230+ <g
2231+ transform="matrix(0,-0.66666667,0.66604479,0,-258.25992,677.00001)"
2232+ id="g875"
2233+ inkscape:label="Layer 1"
2234+ style="fill:#ff00ff;fill-opacity:1;stroke:none;display:inline">
2235+ <path
2236+ style="fill:#ff00ff;fill-opacity:1;stroke:none;display:inline"
2237+ d="m 46.702703,898.22775 50.594594,0 C 138.16216,898.22775 144,904.06497 144,944.92583 l 0,50.73846 c 0,40.86071 -5.83784,46.69791 -46.702703,46.69791 l -50.594594,0 C 5.8378378,1042.3622 0,1036.525 0,995.66429 L 0,944.92583 C 0,904.06497 5.8378378,898.22775 46.702703,898.22775 Z"
2238+ id="path877"
2239+ inkscape:connector-curvature="0"
2240+ sodipodi:nodetypes="sssssssss" />
2241+ </g>
2242+ </clipPath>
2243+ <filter
2244+ inkscape:collect="always"
2245+ id="filter891"
2246+ inkscape:label="Badge Shadow">
2247+ <feGaussianBlur
2248+ inkscape:collect="always"
2249+ stdDeviation="0.71999962"
2250+ id="feGaussianBlur893" />
2251+ </filter>
2252+ <clipPath
2253+ clipPathUnits="userSpaceOnUse"
2254+ id="clipPath874">
2255+ <path
2256+ sodipodi:nodetypes="cccssczcssccccscc"
2257+ inkscape:connector-curvature="0"
2258+ id="path876"
2259+ d="m -414.0975,764.53909 c -7.8125,17.9106 -1.95313,49.75167 -1.95313,49.75167 l 12.69531,0 c 0,-2.9851 -0.83592,-4.55148 -1.19017,-6.62319 -3.77705,-22.08828 -2.54859,-29.19801 -0.76295,-29.19801 1.95312,0 10.74219,24.87583 10.74219,24.87583 0,0 1.95313,-0.49751 3.90625,-0.49751 1.95312,0 3.90625,0.49751 3.90625,0.49751 0,0 8.78906,-24.87583 10.74218,-24.87583 1.78565,0 3.01411,7.10973 -0.76293,29.19801 -0.35426,2.07171 -1.19019,3.63809 -1.19019,6.62319 l 12.69532,0 c 0,0 5.85937,-31.84107 -1.95314,-49.75167 l -11.71874,0 c -3.3378,-0.20005 -10.74219,14.9255 -11.71875,14.9255 C -391.63657,779.46459 -399.04095,764.33904 -402.37875,764.53909 Z"
2260+ style="opacity:0.47400004;color:#000000;fill:#ff00ff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.5;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
2261+ </clipPath>
2262+ <clipPath
2263+ clipPathUnits="userSpaceOnUse"
2264+ id="clipPath896">
2265+ <path
2266+ sodipodi:nodetypes="cccssczcssccccscc"
2267+ inkscape:connector-curvature="0"
2268+ id="path898"
2269+ d="m -414.0975,764.53909 c -7.8125,17.9106 -1.95313,49.75167 -1.95313,49.75167 l 12.69531,0 c 0,-2.9851 -0.83592,-4.55148 -1.19017,-6.62319 -3.77705,-22.08828 -2.54859,-29.19801 -0.76295,-29.19801 1.95312,0 10.74219,24.87583 10.74219,24.87583 0,0 1.95313,-0.49751 3.90625,-0.49751 1.95312,0 3.90625,0.49751 3.90625,0.49751 0,0 8.78906,-24.87583 10.74218,-24.87583 1.78565,0 3.01411,7.10973 -0.76293,29.19801 -0.35426,2.07171 -1.19019,3.63809 -1.19019,6.62319 l 12.69532,0 c 0,0 5.85937,-31.84107 -1.95314,-49.75167 l -11.71874,0 c -3.3378,-0.20005 -10.74219,14.9255 -11.71875,14.9255 C -391.63657,779.46459 -399.04095,764.33904 -402.37875,764.53909 Z"
2270+ style="color:#000000;fill:#ff00ff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.5;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
2271+ </clipPath>
2272+ <linearGradient
2273+ id="linearGradient3354-9">
2274+ <stop
2275+ id="stop3356-9"
2276+ offset="0"
2277+ style="stop-color:#959595;stop-opacity:1;" />
2278+ <stop
2279+ id="stop3358-9"
2280+ offset="1"
2281+ style="stop-color:#cccccc;stop-opacity:1;" />
2282+ </linearGradient>
2283+ <linearGradient
2284+ y2="-32.881535"
2285+ x2="-560.61346"
2286+ y1="-40.681377"
2287+ x1="-403.07309"
2288+ gradientUnits="userSpaceOnUse"
2289+ id="linearGradient4343"
2290+ xlink:href="#linearGradient3354-9"
2291+ inkscape:collect="always" />
2292+ <inkscape:perspective
2293+ sodipodi:type="inkscape:persp3d"
2294+ inkscape:vp_x="0 : 0.5 : 1"
2295+ inkscape:vp_y="0 : 1000 : 0"
2296+ inkscape:vp_z="1 : 0.5 : 1"
2297+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
2298+ id="perspective4393" />
2299+ <inkscape:perspective
2300+ id="perspective4383"
2301+ inkscape:persp3d-origin="372.04724 : 350.78739 : 1"
2302+ inkscape:vp_z="744.09448 : 526.18109 : 1"
2303+ inkscape:vp_y="0 : 1000 : 0"
2304+ inkscape:vp_x="0 : 526.18109 : 1"
2305+ sodipodi:type="inkscape:persp3d" />
2306+ <linearGradient
2307+ inkscape:collect="always"
2308+ xlink:href="#linearGradient3354-9"
2309+ id="linearGradient3164"
2310+ gradientUnits="userSpaceOnUse"
2311+ x1="-403.07309"
2312+ y1="-40.681377"
2313+ x2="-560.61346"
2314+ y2="-32.881535" />
2315+ </defs>
2316+ <sodipodi:namedview
2317+ id="base"
2318+ pagecolor="#ffffff"
2319+ bordercolor="#666666"
2320+ borderopacity="1.0"
2321+ inkscape:pageopacity="0.0"
2322+ inkscape:pageshadow="2"
2323+ inkscape:zoom="2.6077032"
2324+ inkscape:cx="-17.529322"
2325+ inkscape:cy="74.347537"
2326+ inkscape:document-units="px"
2327+ inkscape:current-layer="layer1"
2328+ showgrid="false"
2329+ fit-margin-top="0"
2330+ fit-margin-left="0"
2331+ fit-margin-right="0"
2332+ fit-margin-bottom="0"
2333+ inkscape:window-width="1920"
2334+ inkscape:window-height="1056"
2335+ inkscape:window-x="0"
2336+ inkscape:window-y="24"
2337+ inkscape:window-maximized="1"
2338+ showborder="true"
2339+ showguides="false"
2340+ inkscape:guide-bbox="true"
2341+ inkscape:showpageshadow="false"
2342+ inkscape:snap-global="false"
2343+ inkscape:snap-bbox="true"
2344+ inkscape:bbox-paths="true"
2345+ inkscape:bbox-nodes="true"
2346+ inkscape:snap-bbox-edge-midpoints="true"
2347+ inkscape:snap-bbox-midpoints="true"
2348+ inkscape:snap-intersection-paths="true"
2349+ inkscape:object-paths="true"
2350+ inkscape:object-nodes="true"
2351+ inkscape:snap-smooth-nodes="true"
2352+ inkscape:snap-midpoints="true"
2353+ inkscape:snap-object-midpoints="false"
2354+ inkscape:snap-center="false"
2355+ inkscape:snap-grids="false"
2356+ inkscape:snap-to-guides="false">
2357+ <inkscape:grid
2358+ type="xygrid"
2359+ id="grid821" />
2360+ <sodipodi:guide
2361+ orientation="1,0"
2362+ position="16,48"
2363+ id="guide823" />
2364+ <sodipodi:guide
2365+ orientation="0,1"
2366+ position="64,80"
2367+ id="guide825" />
2368+ <sodipodi:guide
2369+ orientation="1,0"
2370+ position="80,40"
2371+ id="guide827" />
2372+ <sodipodi:guide
2373+ orientation="0,1"
2374+ position="64,16"
2375+ id="guide829" />
2376+ </sodipodi:namedview>
2377+ <metadata
2378+ id="metadata6522">
2379+ <rdf:RDF>
2380+ <cc:Work
2381+ rdf:about="">
2382+ <dc:format>image/svg+xml</dc:format>
2383+ <dc:type
2384+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
2385+ <dc:title></dc:title>
2386+ </cc:Work>
2387+ </rdf:RDF>
2388+ </metadata>
2389+ <g
2390+ inkscape:label="BACKGROUND"
2391+ inkscape:groupmode="layer"
2392+ id="layer1"
2393+ transform="translate(268,-635.29076)"
2394+ style="display:inline">
2395+ <path
2396+ style="fill:url(#linearGradient6461);fill-opacity:1;stroke:none;display:inline;filter:url(#filter1121)"
2397+ d="m -268,700.15563 0,-33.72973 c 0,-27.24324 3.88785,-31.13513 31.10302,-31.13513 l 33.79408,0 c 27.21507,0 31.1029,3.89189 31.1029,31.13513 l 0,33.72973 c 0,27.24325 -3.88783,31.13514 -31.1029,31.13514 l -33.79408,0 C -264.11215,731.29077 -268,727.39888 -268,700.15563 Z"
2398+ id="path6455"
2399+ inkscape:connector-curvature="0"
2400+ sodipodi:nodetypes="sssssssss" />
2401+ <g
2402+ style="display:inline;overflow:visible"
2403+ id="g5"
2404+ transform="matrix(0.19173482,0,0,0.19173482,-268.64964,662.16724)">
2405+ <g
2406+ id="g7">
2407+ <path
2408+ d="m 86.945,33.919 h 23.872 v 110.496 c -12.246,2.325 -21.237,3.255 -31.002,3.255 -29.142,0 -44.333,-13.174 -44.333,-38.443 0,-24.336 16.122,-40.147 41.078,-40.147 3.875,0 6.82,0.311 10.386,1.239 v -36.4 z m 0,55.62 C 84.155,88.61 81.83,88.3 78.885,88.3 c -12.091,0 -19.067,7.441 -19.067,20.46 0,12.713 6.666,19.688 18.912,19.688 2.634,0 4.805,-0.155 8.215,-0.618 V 89.539 z"
2409+ id="path9"
2410+ inkscape:connector-curvature="0"
2411+ style="fill:#ffffff" />
2412+ <path
2413+ d="m 148.793,70.783 v 55.341 c 0,19.065 -1.395,28.21 -5.58,36.117 -3.876,7.596 -8.992,12.399 -19.532,17.67 L 101.514,169.37 c 10.541,-4.96 15.656,-9.297 18.911,-15.966 3.411,-6.819 4.497,-14.727 4.497,-35.498 V 70.783 h 23.871 z M 124.922,34.046 h 23.871 V 58.539 H 124.922 V 34.046 z"
2414+ id="path11"
2415+ inkscape:connector-curvature="0"
2416+ style="fill:#ffffff" />
2417+ <path
2418+ d="m 163.212,76.209 c 10.542,-4.961 20.617,-7.13 31.623,-7.13 12.246,0 20.306,3.255 23.872,9.611 2.014,3.564 2.634,8.214 2.634,18.137 v 48.517 c -10.697,1.552 -24.182,2.636 -34.102,2.636 -19.996,0 -28.988,-6.977 -28.988,-22.476 0,-16.744 11.936,-24.493 41.234,-26.975 v -5.271 c 0,-4.339 -2.17,-5.888 -8.216,-5.888 -8.835,0 -18.756,2.479 -28.058,7.285 V 76.209 z m 37.358,37.978 c -15.812,1.552 -20.927,4.031 -20.927,10.231 0,4.65 2.946,6.821 9.456,6.821 3.566,0 6.82,-0.311 11.471,-1.084 v -15.968 z"
2419+ id="path13"
2420+ inkscape:connector-curvature="0"
2421+ style="fill:#ffffff" />
2422+ <path
2423+ d="m 232.968,74.505 c 14.105,-3.722 25.731,-5.426 37.512,-5.426 12.246,0 21.082,2.788 26.354,8.216 4.96,5.113 6.509,10.693 6.509,22.632 v 46.813 h -23.871 v -45.884 c 0,-9.145 -3.1,-12.557 -11.625,-12.557 -3.255,0 -6.2,0.311 -11.007,1.706 v 56.734 H 232.969 V 74.505 z"
2424+ id="path15"
2425+ inkscape:connector-curvature="0"
2426+ style="fill:#ffffff" />
2427+ <path
2428+ d="m 312.623,159.761 c 8.372,4.339 16.742,6.354 25.577,6.354 15.655,0 22.321,-6.354 22.321,-21.546 0,-0.154 0,-0.31 0,-0.467 -4.65,2.326 -9.301,3.257 -15.5,3.257 -20.927,0 -34.26,-13.797 -34.26,-35.652 0,-27.128 19.688,-42.473 54.564,-42.473 10.232,0 19.688,1.084 31.159,3.407 l -8.174,17.222 c -6.356,-1.241 -0.509,-0.167 -5.312,-0.632 v 2.48 l 0.309,10.074 0.154,13.022 c 0.155,3.253 0.155,6.51 0.311,9.764 0,2.945 0,4.342 0,6.512 0,20.462 -1.705,30.073 -6.82,37.977 -7.441,11.627 -20.307,17.362 -38.598,17.362 -9.301,0 -17.36,-1.396 -25.732,-4.651 v -22.01 z m 47.434,-71.306 c -0.31,0 -0.619,0 -0.774,0 h -1.706 c -4.649,-0.155 -10.074,1.084 -13.796,3.409 -5.734,3.257 -8.681,9.146 -8.681,17.518 0,11.937 5.892,18.756 16.432,18.756 3.255,0 5.891,-0.62 8.99,-1.55 v -1.705 -6.51 c 0,-2.79 -0.154,-5.892 -0.154,-9.146 l -0.154,-11.006 -0.156,-7.905 v -1.861 z"
2429+ id="path17"
2430+ inkscape:connector-curvature="0"
2431+ style="fill:#ffffff" />
2432+ <path
2433+ d="m 433.543,68.77 c 23.871,0 38.443,15.037 38.443,39.371 0,24.957 -15.19,40.613 -39.373,40.613 -23.873,0 -38.599,-15.036 -38.599,-39.216 10e-4,-25.114 15.193,-40.768 39.529,-40.768 z m -0.467,60.763 c 9.147,0 14.573,-7.596 14.573,-20.773 0,-13.019 -5.271,-20.771 -14.415,-20.771 -9.457,0 -14.884,7.598 -14.884,20.771 10e-4,13.178 5.427,20.773 14.726,20.773 z"
2434+ id="path19"
2435+ inkscape:connector-curvature="0"
2436+ style="fill:#ffffff" />
2437+ </g>
2438+ </g>
2439+ </g>
2440+ <g
2441+ inkscape:groupmode="layer"
2442+ id="layer3"
2443+ inkscape:label="PLACE YOUR PICTOGRAM HERE"
2444+ style="display:inline" />
2445+ <g
2446+ inkscape:groupmode="layer"
2447+ id="layer2"
2448+ inkscape:label="BADGE"
2449+ style="display:none"
2450+ sodipodi:insensitive="true">
2451+ <g
2452+ style="display:inline"
2453+ transform="translate(-340.00001,-581)"
2454+ id="g4394"
2455+ clip-path="none">
2456+ <g
2457+ id="g855">
2458+ <g
2459+ inkscape:groupmode="maskhelper"
2460+ id="g870"
2461+ clip-path="url(#clipPath873)"
2462+ style="opacity:0.6;filter:url(#filter891)">
2463+ <path
2464+ transform="matrix(1.4999992,0,0,1.4999992,-29.999795,-237.54282)"
2465+ d="m 264,552.36218 c 0,6.62742 -5.37258,12 -12,12 -6.62742,0 -12,-5.37258 -12,-12 0,-6.62741 5.37258,-12 12,-12 6.62742,0 12,5.37259 12,12 z"
2466+ sodipodi:ry="12"
2467+ sodipodi:rx="12"
2468+ sodipodi:cy="552.36218"
2469+ sodipodi:cx="252"
2470+ id="path844"
2471+ style="color:#000000;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
2472+ sodipodi:type="arc" />
2473+ </g>
2474+ <g
2475+ id="g862">
2476+ <path
2477+ sodipodi:type="arc"
2478+ style="color:#000000;fill:#f5f5f5;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
2479+ id="path4398"
2480+ sodipodi:cx="252"
2481+ sodipodi:cy="552.36218"
2482+ sodipodi:rx="12"
2483+ sodipodi:ry="12"
2484+ d="m 264,552.36218 c 0,6.62742 -5.37258,12 -12,12 -6.62742,0 -12,-5.37258 -12,-12 0,-6.62741 5.37258,-12 12,-12 6.62742,0 12,5.37259 12,12 z"
2485+ transform="matrix(1.4999992,0,0,1.4999992,-29.999795,-238.54282)" />
2486+ <path
2487+ transform="matrix(1.25,0,0,1.25,33,-100.45273)"
2488+ d="m 264,552.36218 c 0,6.62742 -5.37258,12 -12,12 -6.62742,0 -12,-5.37258 -12,-12 0,-6.62741 5.37258,-12 12,-12 6.62742,0 12,5.37259 12,12 z"
2489+ sodipodi:ry="12"
2490+ sodipodi:rx="12"
2491+ sodipodi:cy="552.36218"
2492+ sodipodi:cx="252"
2493+ id="path4400"
2494+ style="color:#000000;fill:#dd4814;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
2495+ sodipodi:type="arc" />
2496+ <path
2497+ sodipodi:type="star"
2498+ style="color:#000000;fill:#f5f5f5;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
2499+ id="path4459"
2500+ sodipodi:sides="5"
2501+ sodipodi:cx="666.19574"
2502+ sodipodi:cy="589.50385"
2503+ sodipodi:r1="7.2431178"
2504+ sodipodi:r2="4.3458705"
2505+ sodipodi:arg1="1.0471976"
2506+ sodipodi:arg2="1.6755161"
2507+ inkscape:flatsided="false"
2508+ inkscape:rounded="0.1"
2509+ inkscape:randomized="0"
2510+ d="m 669.8173,595.77657 c -0.39132,0.22593 -3.62645,-1.90343 -4.07583,-1.95066 -0.44938,-0.0472 -4.05653,1.36297 -4.39232,1.06062 -0.3358,-0.30235 0.68963,-4.03715 0.59569,-4.47913 -0.0939,-0.44198 -2.5498,-3.43681 -2.36602,-3.8496 0.18379,-0.41279 4.05267,-0.59166 4.44398,-0.81759 0.39132,-0.22593 2.48067,-3.48704 2.93005,-3.4398 0.44938,0.0472 1.81505,3.67147 2.15084,3.97382 0.3358,0.30236 4.08294,1.2817 4.17689,1.72369 0.0939,0.44198 -2.9309,2.86076 -3.11469,3.27355 -0.18379,0.41279 0.0427,4.27917 -0.34859,4.5051 z"
2511+ transform="matrix(1.511423,-0.16366377,0.16366377,1.511423,-755.37346,-191.93651)" />
2512+ </g>
2513+ </g>
2514+ </g>
2515+ </g>
2516+</svg>
2517
2518=== modified file 'metadata.yaml'
2519--- metadata.yaml 2012-07-09 16:00:51 +0000
2520+++ metadata.yaml 2013-06-25 15:22:25 +0000
2521@@ -1,28 +1,31 @@
2522 name: python-django
2523 summary: High-level Python web development framework
2524 maintainer: Patrick Hetu <patrick.hetu@gmail.com>
2525+categories: ['databases', 'app-servers']
2526 description: |
2527- Django is a high-level web application framework that loosely follows
2528- the model-view-controller design pattern. Python's equivalent to Ruby
2529- on Rails, Django lets you build complex data-driven websites quickly
2530- and easily - Django focuses on automating as much as possible and
2531- adhering to the "Don't Repeat Yourself" (DRY) principle. Django
2532- additionally emphasizes reusability and "pluggability" of components;
2533- many generic third-party "applications" are available to enhance
2534- projects or to simply to reduce development time even further.
2535- Notable features include: * An object-relational mapper (ORM) *
2536- Automatic admin interface * Elegant URL dispatcher * Form
2537- serialization and validation system * Templating system * Lightweight,
2538- standalone web server for development and testing *
2539- Internationalization support * Testing framework and client
2540+ This charm will install Django. It can also install your Django
2541+ project and his dependencies from either a template or from a
2542+ version control system.
2543+ It can also link your project to a database and sync the schemas.
2544+ This charm also come with a Fabric fabfile to interact with the
2545+ deployement in a cloud aware manner.
2546 provides:
2547 website:
2548 interface: http
2549+ optional: true
2550 wsgi:
2551 interface: wsgi
2552 scope: container
2553+ django-settings:
2554+ interface: directory-path
2555+ scope: container
2556 requires:
2557- db:
2558+ pgsql:
2559 interface: pgsql
2560+ optional: true
2561+ mongodb:
2562+ interface: mongodb
2563+ optional: true
2564 cache:
2565 interface: memcache
2566+ optional: true
2567
2568=== modified file 'revision'
2569--- revision 2012-05-09 20:20:08 +0000
2570+++ revision 2013-06-25 15:22:25 +0000
2571@@ -1,1 +1,1 @@
2572-1
2573+3
2574
2575=== added directory 'templates'
2576=== added file 'templates/cache.tmpl'
2577--- templates/cache.tmpl 1970-01-01 00:00:00 +0000
2578+++ templates/cache.tmpl 2013-06-25 15:22:25 +0000
2579@@ -0,0 +1,10 @@
2580+#--------------------------------------------------------------
2581+# This file is managed by Juju; ANY CHANGES WILL BE OVERWRITTEN
2582+#--------------------------------------------------------------
2583+
2584+CACHES = {
2585+ 'default': {
2586+ 'BACKEND': '{{cache_engine}}',
2587+ 'LOCATION': '{{cache_host}}:{{cache_port}}',
2588+ }
2589+}
2590
2591=== added file 'templates/cloudfiles.tmpl'
2592--- templates/cloudfiles.tmpl 1970-01-01 00:00:00 +0000
2593+++ templates/cloudfiles.tmpl 2013-06-25 15:22:25 +0000
2594@@ -0,0 +1,48 @@
2595+#--------------------------------------------------------------
2596+# This file is managed by Juju; ANY CHANGES WILL BE OVERWRITTEN
2597+#--------------------------------------------------------------
2598+
2599+import os
2600+
2601+PROJECT_ROOT = os.path.dirname(__file__)
2602+
2603+class SwiftAuthentication(object):
2604+ """Auth container to pass CloudFiles storage URL and token from
2605+ session.
2606+ """
2607+ def __init__(self, auth_url, username, password, tenant_id):
2608+ self.auth_url = auth_url
2609+ self.username = username
2610+ self.password = password
2611+ self.tenant_id = tenant_id
2612+
2613+ def authenticate(self):
2614+ from keystoneclient.v2_0 import client as ksclient
2615+ _ksclient = ksclient.Client(username=self.username,
2616+ password=self.password,
2617+ tenant_id=self.tenant_id,
2618+ auth_url=self.auth_url)
2619+ endpoint = _ksclient.service_catalog.url_for(service_type='object-store',
2620+ endpoint_type='publicURL')
2621+
2622+ return (endpoint, '', _ksclient.auth_token)
2623+
2624+auth = SwiftAuthentication('$SWIFT_AUTH_URL', '$SWIFT_USERNAME', '$SWIFT_PASSWORD', '$SWIFT_TENANTID')
2625+
2626+MEDIA_URL = "$SWIFT_ENDPOINT_URL/$SWIFT_VERSION/$SWIFT_TENANTID/$SWIFT_CONTAINER_NAME/uploads/"
2627+
2628+ADMIN_MEDIA_PREFIX = '$SWIFT_ENDPOINT_URL/$SWIFT_VERSION/$SWIFT_TENANTID/$SWIFT_CONTAINER_NAME/static/admin/'
2629+
2630+STATICFILES_URL = '$SWIFT_ENDPOINT_URL/$SWIFT_VERSION/$SWIFT_TENANTID/$SWIFT_CONTAINER_NAME/static/'
2631+STATIC_URL = STATICFILES_URL
2632+
2633+DEFAULT_FILE_STORAGE = 'cumulus.storage.CloudFilesStorage'
2634+STATICFILES_STORAGE = 'cumulus.storage.CloudFilesStaticStorage'
2635+
2636+CUMULUS = {
2637+ 'CONNECTION_ARGS': {'auth' : auth},
2638+ 'CONTAINER': '$SWIFT_CONTAINER_NAME'
2639+}
2640+
2641+COMPRESS_STORAGE = "cumulus.storage.CachedCloudFilesStaticStorage"
2642+
2643
2644=== added file 'templates/conf_injection.tmpl'
2645--- templates/conf_injection.tmpl 1970-01-01 00:00:00 +0000
2646+++ templates/conf_injection.tmpl 2013-06-25 15:22:25 +0000
2647@@ -0,0 +1,10 @@
2648+import glob
2649+from os.path import abspath, dirname, join
2650+
2651+PROJECT_DIR = abspath(dirname(__file__))
2652+
2653+conffiles = glob.glob(join(PROJECT_DIR, '{{ dir }}', '*.py'))
2654+conffiles.sort()
2655+
2656+for f in conffiles:
2657+ execfile(abspath(f))
2658
2659=== added file 'templates/engine.tmpl'
2660--- templates/engine.tmpl 1970-01-01 00:00:00 +0000
2661+++ templates/engine.tmpl 2013-06-25 15:22:25 +0000
2662@@ -0,0 +1,19 @@
2663+#--------------------------------------------------------------
2664+# This file is managed by Juju; ANY CHANGES WILL BE OVERWRITTEN
2665+#--------------------------------------------------------------
2666+
2667+DATABASES = {
2668+ "default": {
2669+ "ENGINE": '{{ db_engine }}',
2670+ "NAME": '{{ db_database }}',
2671+ "USER": '{{ db_user }}',
2672+ "PASSWORD": '{{ db_password }}',
2673+ "HOST": '{{ db_host }}',
2674+ "PORT": '',
2675+ "OPTIONS": {'autocommit': True},
2676+ }
2677+}
2678+
2679+# Backward compatibility
2680+DATABASE_ENGINE=DATABASES
2681+
2682
2683=== added file 'templates/mongodb_engine.tmpl'
2684--- templates/mongodb_engine.tmpl 1970-01-01 00:00:00 +0000
2685+++ templates/mongodb_engine.tmpl 2013-06-25 15:22:25 +0000
2686@@ -0,0 +1,8 @@
2687+#--------------------------------------------------------------
2688+# This file is managed by Juju; ANY CHANGES WILL BE OVERWRITTEN
2689+#--------------------------------------------------------------
2690+
2691+MONGO_DB = {'name': '{{ db_database }}',
2692+ 'host': '{{ db_host }}',
2693+ 'port': 27017
2694+}
2695
2696=== added file 'templates/netrc.tmpl'
2697--- templates/netrc.tmpl 1970-01-01 00:00:00 +0000
2698+++ templates/netrc.tmpl 2013-06-25 15:22:25 +0000
2699@@ -0,0 +1,10 @@
2700+#--------------------------------------------------------------
2701+# This file is managed by Juju; ANY CHANGES WILL BE OVERWRITTEN
2702+#--------------------------------------------------------------
2703+
2704+# Netrc configuration for VCS repo
2705+
2706+machine {{ repos_domain }}
2707+ login {{ repos_username }}
2708+ password {{ repos_password }}
2709+
2710
2711=== added file 'templates/secret.tmpl'
2712--- templates/secret.tmpl 1970-01-01 00:00:00 +0000
2713+++ templates/secret.tmpl 2013-06-25 15:22:25 +0000
2714@@ -0,0 +1,5 @@
2715+#--------------------------------------------------------------
2716+# This file is managed by Juju; ANY CHANGES WILL BE OVERWRITTEN
2717+#--------------------------------------------------------------
2718+
2719+SECRET_KEY = '{{site_secret_key}}'
2720
2721=== added file 'templates/wsgi.py.tmpl'
2722--- templates/wsgi.py.tmpl 1970-01-01 00:00:00 +0000
2723+++ templates/wsgi.py.tmpl 2013-06-25 15:22:25 +0000
2724@@ -0,0 +1,12 @@
2725+#--------------------------------------------------------------
2726+# This file is managed by Juju; ANY CHANGES WILL BE OVERWRITTEN
2727+#--------------------------------------------------------------
2728+
2729+import os
2730+import sys
2731+
2732+os.environ['DJANGO_SETTINGS_MODULE'] = '{{ project_name }}.{{ django_settings }}'
2733+
2734+import django.core.handlers.wsgi
2735+application = django.core.handlers.wsgi.WSGIHandler()
2736+
2737
2738=== added directory 'tests'
2739=== added file 'tests/01_deploy.test'
2740--- tests/01_deploy.test 1970-01-01 00:00:00 +0000
2741+++ tests/01_deploy.test 2013-06-25 15:22:25 +0000
2742@@ -0,0 +1,51 @@
2743+#!/usr/bin/python
2744+# Copyright 2012 Canonical Ltd. This software is licensed under the
2745+# GNU Affero General Public License version 3 (see the file LICENSE).
2746+
2747+from helpers import (
2748+ command,
2749+ make_charm_config_file,
2750+ unit_info,
2751+ wait_for_page_contents,
2752+ )
2753+import unittest
2754+
2755+juju = command('juju')
2756+
2757+
2758+class TestCharm(unittest.TestCase):
2759+
2760+ def tearDown(self):
2761+ juju('destroy-service', 'postgresql')
2762+ juju('destroy-service', 'python-django')
2763+ juju('destroy-service', 'gunicorn')
2764+
2765+ def deploy(self, charm_config=None):
2766+ if charm_config is not None:
2767+ charm_config_file = make_charm_config_file(charm_config)
2768+ juju('deploy', 'postgresql')
2769+ juju('deploy', '--config=' + charm_config_file.name, 'python-django')
2770+ juju('deploy', 'gunicorn')
2771+ juju('add-relation', 'python-django:db', 'postgresql:db')
2772+ juju('add-relation', 'python-django', 'gunicorn')
2773+
2774+
2775+ def expose_and_check_page(self):
2776+ juju('expose', 'python-django')
2777+ addr = unit_info('python-django', 'public-address')
2778+ url = 'http://{}:8080/admin/'.format(addr)
2779+ wait_for_page_contents(url, 'Administration de Django', timeout=1000)
2780+
2781+ def get_config(self):
2782+ return {
2783+ 'site_secret_key': 'abcdefghijklmmnopqrstuvwxyz'
2784+ }
2785+
2786+ def test_port_opened(self):
2787+ # Deploying a buildbot master should result in it opening a port and
2788+ # serving its status via HTTP.
2789+ self.deploy(self.get_config())
2790+ self.expose_and_check_page()
2791+
2792+if __name__ == '__main__':
2793+ unittest.main()
2794
2795=== added file 'tests/helpers.py'
2796--- tests/helpers.py 1970-01-01 00:00:00 +0000
2797+++ tests/helpers.py 2013-06-25 15:22:25 +0000
2798@@ -0,0 +1,278 @@
2799+# Copyright 2012 Canonical Ltd. This software is licensed under the
2800+# GNU Affero General Public License version 3 (see the file LICENSE).
2801+
2802+"""Helper functions for writing Juju charms in Python."""
2803+
2804+__metaclass__ = type
2805+__all__ = [
2806+ 'get_config',
2807+ 'log',
2808+ 'log_entry',
2809+ 'log_exit',
2810+ 'relation_get',
2811+ 'relation_set',
2812+ 'relation_ids',
2813+ 'relation_list',
2814+ 'config_get',
2815+ 'unit_get',
2816+ 'open_port',
2817+ 'close_port',
2818+ 'service_control',
2819+ 'unit_info',
2820+ 'wait_for_machine',
2821+ 'wait_for_page_contents',
2822+ 'wait_for_relation',
2823+ 'wait_for_unit',
2824+ ]
2825+
2826+from collections import namedtuple
2827+import json
2828+import operator
2829+from shelltoolbox import (
2830+ command,
2831+ script_name,
2832+ run
2833+ )
2834+import tempfile
2835+import time
2836+import urllib2
2837+import yaml
2838+from subprocess import CalledProcessError
2839+
2840+
2841+SLEEP_AMOUNT = 0.1
2842+Env = namedtuple('Env', 'uid gid home')
2843+# We create a juju_status Command here because it makes testing much,
2844+# much easier.
2845+juju_status = lambda: command('juju')('status')
2846+
2847+
2848+def log(message, juju_log=command('juju-log')):
2849+ return juju_log('--', message)
2850+
2851+
2852+def log_entry():
2853+ log("--> Entering {}".format(script_name()))
2854+
2855+
2856+def log_exit():
2857+ log("<-- Exiting {}".format(script_name()))
2858+
2859+
2860+def get_config():
2861+ _config_get = command('config-get', '--format=json')
2862+ return json.loads(_config_get())
2863+
2864+
2865+def relation_get(attribute=None, unit=None, rid=None):
2866+ cmd = command('relation-get')
2867+ if attribute is None and unit is None and rid is None:
2868+ return cmd().strip()
2869+ _args = []
2870+ if rid:
2871+ _args.append('-r')
2872+ _args.append(rid)
2873+ if attribute is not None:
2874+ _args.append(attribute)
2875+ if unit:
2876+ _args.append(unit)
2877+ return cmd(*_args).strip()
2878+
2879+
2880+def relation_set(**kwargs):
2881+ cmd = command('relation-set')
2882+ args = ['{}={}'.format(k, v) for k, v in kwargs.items()]
2883+ cmd(*args)
2884+
2885+
2886+def relation_ids(relation_name):
2887+ cmd = command('relation-ids')
2888+ args = [relation_name]
2889+ return cmd(*args).split()
2890+
2891+
2892+def relation_list(rid=None):
2893+ cmd = command('relation-list')
2894+ args = []
2895+ if rid:
2896+ args.append('-r')
2897+ args.append(rid)
2898+ return cmd(*args).split()
2899+
2900+
2901+def config_get(attribute):
2902+ cmd = command('config-get')
2903+ args = [attribute]
2904+ return cmd(*args).strip()
2905+
2906+
2907+def unit_get(attribute):
2908+ cmd = command('unit-get')
2909+ args = [attribute]
2910+ return cmd(*args).strip()
2911+
2912+
2913+def open_port(port, protocol="TCP"):
2914+ cmd = command('open-port')
2915+ args = ['{}/{}'.format(port, protocol)]
2916+ cmd(*args)
2917+
2918+
2919+def close_port(port, protocol="TCP"):
2920+ cmd = command('close-port')
2921+ args = ['{}/{}'.format(port, protocol)]
2922+ cmd(*args)
2923+
2924+START = "start"
2925+RESTART = "restart"
2926+STOP = "stop"
2927+RELOAD = "reload"
2928+
2929+
2930+def service_control(service_name, action):
2931+ cmd = command('service')
2932+ args = [service_name, action]
2933+ try:
2934+ if action == RESTART:
2935+ try:
2936+ cmd(*args)
2937+ except CalledProcessError:
2938+ service_control(service_name, START)
2939+ else:
2940+ cmd(*args)
2941+ except CalledProcessError:
2942+ log("Failed to perform {} on service {}".format(action, service_name))
2943+
2944+
2945+def configure_source(update=False):
2946+ source = config_get('source')
2947+ if (source.startswith('ppa:') or
2948+ source.startswith('cloud:') or
2949+ source.startswith('http:')):
2950+ run('add-apt-repository', source)
2951+ if source.startswith("http:"):
2952+ run('apt-key', 'import', config_get('key'))
2953+ if update:
2954+ run('apt-get', 'update')
2955+
2956+
2957+def make_charm_config_file(charm_config):
2958+ charm_config_file = tempfile.NamedTemporaryFile()
2959+ charm_config_file.write(yaml.dump(charm_config))
2960+ charm_config_file.flush()
2961+ # The NamedTemporaryFile instance is returned instead of just the name
2962+ # because we want to take advantage of garbage collection-triggered
2963+ # deletion of the temp file when it goes out of scope in the caller.
2964+ return charm_config_file
2965+
2966+
2967+def unit_info(service_name, item_name, data=None, unit=None):
2968+ if data is None:
2969+ data = yaml.safe_load(juju_status())
2970+ service = data['services'].get(service_name)
2971+ if service is None:
2972+ # XXX 2012-02-08 gmb:
2973+ # This allows us to cope with the race condition that we
2974+ # have between deploying a service and having it come up in
2975+ # `juju status`. We could probably do with cleaning it up so
2976+ # that it fails a bit more noisily after a while.
2977+ return ''
2978+ units = service['units']
2979+ if unit is not None:
2980+ item = units[unit][item_name]
2981+ else:
2982+ # It might seem odd to sort the units here, but we do it to
2983+ # ensure that when no unit is specified, the first unit for the
2984+ # service (or at least the one with the lowest number) is the
2985+ # one whose data gets returned.
2986+ sorted_unit_names = sorted(units.keys())
2987+ item = units[sorted_unit_names[0]][item_name]
2988+ return item
2989+
2990+
2991+def get_machine_data():
2992+ return yaml.safe_load(juju_status())['machines']
2993+
2994+
2995+def wait_for_machine(num_machines=1, timeout=300):
2996+ """Wait `timeout` seconds for `num_machines` machines to come up.
2997+
2998+ This wait_for... function can be called by other wait_for functions
2999+ whose timeouts might be too short in situations where only a bare
3000+ Juju setup has been bootstrapped.
3001+
3002+ :return: A tuple of (num_machines, time_taken). This is used for
3003+ testing.
3004+ """
3005+ # You may think this is a hack, and you'd be right. The easiest way
3006+ # to tell what environment we're working in (LXC vs EC2) is to check
3007+ # the dns-name of the first machine. If it's localhost we're in LXC
3008+ # and we can just return here.
3009+ if get_machine_data()[0]['dns-name'] == 'localhost':
3010+ return 1, 0
3011+ start_time = time.time()
3012+ while True:
3013+ # Drop the first machine, since it's the Zookeeper and that's
3014+ # not a machine that we need to wait for. This will only work
3015+ # for EC2 environments, which is why we return early above if
3016+ # we're in LXC.
3017+ machine_data = get_machine_data()
3018+ non_zookeeper_machines = [
3019+ machine_data[key] for key in machine_data.keys()[1:]]
3020+ if len(non_zookeeper_machines) >= num_machines:
3021+ all_machines_running = True
3022+ for machine in non_zookeeper_machines:
3023+ if machine.get('instance-state') != 'running':
3024+ all_machines_running = False
3025+ break
3026+ if all_machines_running:
3027+ break
3028+ if time.time() - start_time >= timeout:
3029+ raise RuntimeError('timeout waiting for service to start')
3030+ time.sleep(SLEEP_AMOUNT)
3031+ return num_machines, time.time() - start_time
3032+
3033+
3034+def wait_for_unit(service_name, timeout=480):
3035+ """Wait `timeout` seconds for a given service name to come up."""
3036+ wait_for_machine(num_machines=1)
3037+ start_time = time.time()
3038+ while True:
3039+ state = unit_info(service_name, 'agent-state')
3040+ if 'error' in state or state == 'started':
3041+ break
3042+ if time.time() - start_time >= timeout:
3043+ raise RuntimeError('timeout waiting for service to start')
3044+ time.sleep(SLEEP_AMOUNT)
3045+ if state != 'started':
3046+ raise RuntimeError('unit did not start, agent-state: ' + state)
3047+
3048+
3049+def wait_for_relation(service_name, relation_name, timeout=120):
3050+ """Wait `timeout` seconds for a given relation to come up."""
3051+ start_time = time.time()
3052+ while True:
3053+ relation = unit_info(service_name, 'relations').get(relation_name)
3054+ if relation is not None and relation['state'] == 'up':
3055+ break
3056+ if time.time() - start_time >= timeout:
3057+ raise RuntimeError('timeout waiting for relation to be up')
3058+ time.sleep(SLEEP_AMOUNT)
3059+
3060+
3061+def wait_for_page_contents(url, contents, timeout=120, validate=None):
3062+ if validate is None:
3063+ validate = operator.contains
3064+ start_time = time.time()
3065+ while True:
3066+ try:
3067+ stream = urllib2.urlopen(url)
3068+ except (urllib2.HTTPError, urllib2.URLError):
3069+ pass
3070+ else:
3071+ page = stream.read()
3072+ if validate(page, contents):
3073+ return page
3074+ if time.time() - start_time >= timeout:
3075+ raise RuntimeError('timeout waiting for contents of ' + url)
3076+ time.sleep(SLEEP_AMOUNT)

Subscribers

People subscribed via source and target branches

to all changes: