Merge lp:~patrick-hetu/charms/precise/python-django/python-rewrite into lp:~charmers/charms/precise/python-django/trunk
- Precise Pangolin (12.04)
- python-rewrite
- Merge into 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 | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Mark Mims (community) | Approve | ||
Review via email: mp+171333@code.launchpad.net |
Commit message
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 : | # |
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) |
When this lands I'd be more than happy to fix the README