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

Proposed by Patrick Hetu
Status: Work in progress
Proposed branch: lp:~patrick-hetu/charms/precise/python-django/charmhelpers
Merge into: lp:~charmers/charms/precise/python-django/trunk
Diff against target: 6541 lines (+4272/-1374)
106 files modified
Makefile (+29/-0)
README.md (+86/-16)
ansible.py (+30/-0)
bin/charm_helpers_sync.py (+225/-0)
charm-helpers.yaml (+7/-0)
config.yaml (+99/-22)
dev/ubuntu-deps (+17/-0)
fabfile.py (+58/-33)
hooks/charmhelpers/contrib/ansible/__init__.py (+165/-0)
hooks/charmhelpers/contrib/templating/contexts.py (+104/-0)
hooks/charmhelpers/core/fstab.py (+114/-0)
hooks/charmhelpers/core/hookenv.py (+498/-0)
hooks/charmhelpers/core/host.py (+325/-0)
hooks/charmhelpers/fetch/__init__.py (+349/-0)
hooks/charmhelpers/fetch/archiveurl.py (+63/-0)
hooks/charmhelpers/fetch/bzrurl.py (+50/-0)
hooks/hooks.py (+34/-850)
hooks/start (+1/-0)
hooks/stop (+1/-0)
metadata.yaml (+17/-1)
playbooks/roles/README.md (+4/-0)
playbooks/roles/aptkit/defaults/main.yml (+18/-0)
playbooks/roles/aptkit/tasks/main.yml (+31/-0)
playbooks/roles/django-app/handlers/main.yml (+7/-0)
playbooks/roles/django-app/tasks/main.yml (+67/-0)
playbooks/roles/django-app/templates/amqp.py.j2 (+13/-0)
playbooks/roles/django-app/templates/cache.py.j2 (+10/-0)
playbooks/roles/django-app/templates/cloudfiles.py.j2 (+48/-0)
playbooks/roles/django-app/templates/mongodb.py.j2 (+8/-0)
playbooks/roles/django-app/templates/mysql.py.j2 (+18/-0)
playbooks/roles/django-app/templates/pgsql.py.j2 (+19/-0)
playbooks/roles/django-app/templates/redis.py.j2 (+9/-0)
playbooks/roles/django-app/vars/main.yml (+2/-0)
playbooks/roles/django-project/defaults/main.yml (+64/-0)
playbooks/roles/django-project/tasks/dynamic_vars.yml (+45/-0)
playbooks/roles/django-project/tasks/main.yml (+42/-0)
playbooks/roles/django-project/vars/main.yml (+3/-0)
playbooks/roles/django-settings-injection/handlers/main.yml (+16/-0)
playbooks/roles/django-settings-injection/tasks/main.yml (+73/-0)
playbooks/roles/django-settings-injection/templates/allowed_hosts.py.j2 (+9/-0)
playbooks/roles/django-settings-injection/templates/amqp_celery.py.j2 (+13/-0)
playbooks/roles/django-settings-injection/templates/cache.py.j2 (+10/-0)
playbooks/roles/django-settings-injection/templates/cloudfiles.py.j2 (+48/-0)
playbooks/roles/django-settings-injection/templates/conf_injection.py.j2 (+18/-0)
playbooks/roles/django-settings-injection/templates/debug.py.j2 (+5/-0)
playbooks/roles/django-settings-injection/templates/extra-conf.py.j2 (+7/-0)
playbooks/roles/django-settings-injection/templates/mongodb_engine.py.j2 (+8/-0)
playbooks/roles/django-settings-injection/templates/mysql_engine.py.j2 (+18/-0)
playbooks/roles/django-settings-injection/templates/pgsql_engine.py.j2 (+19/-0)
playbooks/roles/django-settings-injection/templates/redis_engine.py.j2 (+9/-0)
playbooks/roles/django-settings-injection/templates/secret.py.j2 (+5/-0)
playbooks/roles/django-settings-injection/templates/urls_injection.py.j2 (+18/-0)
playbooks/roles/django-settings-injection/templates/wsgi.py.j2 (+12/-0)
playbooks/roles/django-settings-injection/vars/main.yml (+2/-0)
playbooks/roles/nrpe-external-master/defaults/main.yml (+5/-0)
playbooks/roles/nrpe-external-master/tasks/main.yml (+25/-0)
playbooks/roles/nrpe-external-master/templates/check_name.cfg.jinja2 (+4/-0)
playbooks/roles/nrpe-external-master/templates/check_name_service_export.cfg.jinja2 (+10/-0)
playbooks/roles/pip/defaults/main.yml (+14/-0)
playbooks/roles/pip/tasks/main.yml (+28/-0)
playbooks/roles/unit-config/tasks/main.yml (+4/-0)
playbooks/roles/upstart/default/main.yml (+1/-0)
playbooks/roles/upstart/tasks/main.yml (+13/-0)
playbooks/roles/upstart/templates/upstart.conf.j2 (+47/-0)
playbooks/roles/vcs/defaults/main.yml (+22/-0)
playbooks/roles/vcs/tasks/main.yml (+44/-0)
playbooks/roles/wsgi-app/README (+29/-0)
playbooks/roles/wsgi-app/defaults/main.yml (+7/-0)
playbooks/roles/wsgi-app/handlers/main.yml (+7/-0)
playbooks/roles/wsgi-app/tasks/main.yml (+78/-0)
playbooks/roles/wsgi-app/tasks/setup-code.yml (+54/-0)
playbooks/roles/wsgi-app/tasks/setup-machine.yml (+44/-0)
playbooks/roles/wsgi-app/templates/log-rotate.j2 (+10/-0)
playbooks/roles/wsgi-app/vars/main.yml (+7/-0)
playbooks/site.yml (+271/-0)
playbooks/templates/allowed_hosts.py.j2 (+9/-0)
playbooks/templates/amqp_celery.py.j2 (+13/-0)
playbooks/templates/cache.py.j2 (+10/-0)
playbooks/templates/cloudfiles.py.j2 (+48/-0)
playbooks/templates/conf_injection.py.j2 (+18/-0)
playbooks/templates/debug.py.j2 (+5/-0)
playbooks/templates/extra-conf.py.j2 (+7/-0)
playbooks/templates/mongodb_engine.py.j2 (+8/-0)
playbooks/templates/mysql_engine.py.j2 (+18/-0)
playbooks/templates/pgsql_engine.py.j2 (+19/-0)
playbooks/templates/redis_engine.py.j2 (+9/-0)
playbooks/templates/secret.py.j2 (+5/-0)
playbooks/templates/urls_injection.py.j2 (+18/-0)
playbooks/templates/wsgi.py.j2 (+12/-0)
revision (+1/-1)
templates/cache.tmpl (+0/-10)
templates/cloudfiles.tmpl (+0/-48)
templates/conf_injection.tmpl (+0/-10)
templates/engine.tmpl (+0/-19)
templates/mongodb_engine.tmpl (+0/-8)
templates/netrc.tmpl (+0/-10)
templates/secret.tmpl (+0/-5)
templates/wsgi.py.tmpl (+0/-12)
test_requirements.txt (+2/-0)
tests/01-versions (+64/-0)
tests/01_deploy.test (+0/-51)
tests/10-postgresql (+76/-0)
tests/config/django.yaml (+73/-0)
tests/helpers.py (+0/-278)
tests/helpers/__init__.py (+118/-0)
tests/jujulib/deployer.py (+45/-0)
To merge this branch: bzr merge lp:~patrick-hetu/charms/precise/python-django/charmhelpers
Reviewer Review Type Date Requested Status
Charles Butler (community) Needs Fixing
Review via email: mp+216351@code.launchpad.net

Description of the change

- The charm now use charm-helpers for hooks
- The chams now use ansible for installation
- Mysql and Redis support was added
- The charm can install South and do migrations (code from Ubuntu's ci-services)
- The charm include a Fabric and Ansible scripts to interact with Juju

To post a comment you must log in.
50. By Patrick Hetu

forgot mirror urls in the playbook

51. By Patrick Hetu

fix settings path in the fabfile

52. By Patrick Hetu

add a new pip_extra_args option

53. By Patrick Hetu

don't make python-path starts with a colon

54. By Patrick Hetu

fix single python-path for fabfile.py

55. By Patrick Hetu

add rabbitmq support

56. By Patrick Hetu

drop http pip requirements as I can get it to work with ansible

57. By Patrick Hetu

awk to the rescue: Re-enable http pip requirements support

58. By Patrick Hetu

merge george-edison55's readme corrections

59. By Patrick Hetu

add configurable amqp vhost

60. By Patrick Hetu

merge with trunk

61. By Patrick Hetu

fix a wrong configuration variable name

Revision history for this message
Charles Butler (lazypower) wrote :

Patrick,

Wow, this was a pretty significant change! I've reviewed the incoming changes and have the following notes:

This is an interesting mix of Ansible and plane ole python hook code. What was the prompt to move the cloning bits to Ansible vs just using the python hooks for installation? Not a nack, just really curious about the architecture design pattern.

Knitpicks:
There is a missing 00-setup script for the included test, and missing dependencies prevented the test execution from completing successfully on first run.

The tests also report errors as is, and are a prime candidate for being rewritten in amulet, or refactored as is so they pass.

Aside from these comments, I see no major blockers.

Thanks again! +1 LGTM

Revision history for this message
Charles Butler (lazypower) :
review: Approve
Revision history for this message
Patrick Hetu (patrick-hetu) wrote :

About Ansible, I'm still new to all this but in the next merge requests
it should be more consistent and use only Ansible.

Same thing for the tests, I'll switch to unittest + amulet in the next MR.

62. By Patrick Hetu

get the latest version of charmhelpers

63. By Patrick Hetu

try to find settings.py in application_path if it is set

Revision history for this message
Patrick Hetu (patrick-hetu) wrote :

commit 62 fixes bug #1318036

64. By Patrick Hetu

migrate to Ansible

65. By Patrick Hetu

pull playbooks and minor fixes

66. By Patrick Hetu

always install django first

67. By Patrick Hetu

refactoring for reusability

68. By Patrick Hetu

tests refactoring

69. By Patrick Hetu

separation of test in django versions and postgresql tests

70. By Patrick Hetu

test only version distro, 1.3 and 1.4

71. By Patrick Hetu

test only version distro, 1.3 and 1.4

Revision history for this message
Charles Butler (lazypower) wrote :

Greetings Patrick,

Looks like progress has continued to land on the Python-Django charm. I've taken the liberty of reviewing the most recent revision and I have the following notes:

There is a proof error:
W: config.yaml: option django_uid does not have the keys: description
W: config.yaml: option django_gid does not have the keys: description

and I also ran into some troubles with the included integration tests. Attaching tracebacks:

2014-09-08 21:05:12,160 INFO Unavailable, retrying: http://ec2-54-167-142-245.compute-1.amazonaws.com:8080/
ERROR

======================================================================
ERROR: setUpModule (__main__)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "tests/10-postgresql", line 36, in setUpModule
    attempts=10, retry_unavailable=True)
  File "/home/ubuntu/charms/precise/python-django/tests/helpers/__init__.py", line 51, in check_url
    sys.stdout.flush()
BrokenPipeError: [Errno 32] Broken pipe

----------------------------------------------------------------------
Ran 0 tests in 679.730s

FAILED (errors=1)
Exception ignored in: <_io.TextIOWrapper name='<stdout>' mode='w' encoding='ANSI_X3.4-1968'>
BrokenPipeError: [Errno 32] Broken pipe
2014-09-08 21:05:55,425 INFO Unavailable, retrying: http://ec2-54-234-57-45.compute-1.amazonaws.com:8080/
ERROR

======================================================================
ERROR: test_app (01-versions.LandscapeServiceTests)
Verify that the APP service is up.
----------------------------------------------------------------------
Traceback (most recent call last):
  File "tests/01-versions", line 58, in test_app
    attempts=10, retry_unavailable=True)
  File "/home/ubuntu/charms/precise/python-django/tests/helpers/__init__.py", line 51, in check_url
    sys.stdout.flush()
BrokenPipeError: [Errno 32] Broken pipe

----------------------------------------------------------------------
Ran 1 test in 1558.975s

FAILED (errors=1)
Exception ignored in: <_io.TextIOWrapper name='<stdout>' mode='w' encoding='ANSI_X3.4-1968'>
BrokenPipeError: [Errno 32] Broken pipe

Thanks again for the submission, I look forward to the next iteration and progress!

review: Needs Fixing

Unmerged revisions

71. By Patrick Hetu

test only version distro, 1.3 and 1.4

70. By Patrick Hetu

test only version distro, 1.3 and 1.4

69. By Patrick Hetu

separation of test in django versions and postgresql tests

68. By Patrick Hetu

tests refactoring

67. By Patrick Hetu

refactoring for reusability

66. By Patrick Hetu

always install django first

65. By Patrick Hetu

pull playbooks and minor fixes

64. By Patrick Hetu

migrate to Ansible

63. By Patrick Hetu

try to find settings.py in application_path if it is set

62. By Patrick Hetu

get the latest version of charmhelpers

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'Makefile'
2--- Makefile 1970-01-01 00:00:00 +0000
3+++ Makefile 2014-07-07 20:28:28 +0000
4@@ -0,0 +1,29 @@
5+#!/usr/bin/make
6+PYTHON := /usr/bin/env python
7+
8+test: lint integration-test
9+
10+sync-charm-helpers: bin/charm_helpers_sync.py
11+ @mkdir -p bin
12+ @$(PYTHON) bin/charm_helpers_sync.py -c charm-helpers.yaml
13+
14+bin/charm_helpers_sync.py:
15+ @bzr cat lp:charm-helpers/tools/charm_helpers_sync/charm_helpers_sync.py > bin/charm_helpers_sync.py
16+
17+lint:
18+ @echo "Lint check (flake8)"
19+ @flake8 -v --ignore E501 --exclude hooks/charmhelpers hooks
20+
21+verify-juju-test:
22+ @echo "Checking for ... "
23+ @echo -n "juju-test: "
24+ @if [ -z `which juju-test` ]; then \
25+ echo -e "\nRun ./dev/ubuntu-deps to get the juju-test command installed"; \
26+ exit 1;\
27+ else \
28+ echo "installed"; \
29+ fi
30+
31+integration-test:
32+ juju test --set-e -p SKIP_SLOW_TESTS,DEPLOYER_TARGET,JUJU_HOME,JUJU_ENV -v --timeout 3000s
33+
34
35=== modified file 'README.md'
36--- README.md 2014-04-05 21:46:36 +0000
37+++ README.md 2014-07-07 20:28:28 +0000
38@@ -1,8 +1,8 @@
39 # Python-django Charm
40
41-Authors:
42+Authors:
43
44-- Patrick Hetu <patrick.hetu@gmail.com>
45+- Patrick Hetu <patrick.hetu@gmail.com>
46 - Bruno Girin
47
48 ## What is Django?
49@@ -16,7 +16,7 @@
50 many generic third-party "applications" are available to enhance
51 projects or to simply reduce development time even further.
52
53-Notable features include:
54+Notable features include:
55
56 - An object-relational mapper (ORM)
57 - Automatic admin interface
58@@ -66,9 +66,6 @@
59 project_template_url: https://github.com/xenith/django-base-template/zipball/master
60 project_template_extension: py,md,rst
61
62- Note: If you're using juju-core you must remove the first line
63- of the file and the indentation for the rest of the file.
64-
65 1. Deployment with `Gunicorn`:
66
67 juju bootstrap
68@@ -90,23 +87,17 @@
69 vcs: bzr
70 repos_url: lp:~patrick-hetu/my_site
71
72- Note: If you're using juju-core you must remove the first line
73- of the file and the indentation for the rest of the file.
74-
75 1. Deployment with `Gunicorn`:
76
77 juju bootstrap
78- juju deploy --config mydjangosite.yaml python-django
79+ juju deploy --config mydjangosite.yaml python-django mydjangosite
80
81 juju deploy postgresql
82 juju add-relation python-django postgresql:db
83
84 juju deploy gunicorn
85- juju add-relation python-django gunicorn
86- juju expose python-django
87-
88- Note: If you're using juju-core you must add --upload-tools to the
89- `juju bootstrap` command.
90+ juju add-relation mydjangosite gunicorn
91+ juju expose mydjangosite
92
93 1. Your new Django site should be accessible at the public address of
94 Gunicorn. To find it, look for it in the output of the `juju status` command.
95@@ -115,7 +106,7 @@
96
97 Continuing from the previous example, your web site should be on the Django node at:
98
99- /srv/python-django/
100+ /srv/mydjangosite/
101
102 As you can see there the charm has injected some code at the end of your settings.py
103 file (or created it if it was not there) to be able to import what's in the
104@@ -124,6 +115,23 @@
105 It's recommended that you make your vcs ignore database and secret files or
106 any files that have information that you don't want to publish.
107
108+## Complex configuration example: dpaste
109+
110+ mydpastesite:
111+ django_version: ''
112+ django_south: True
113+ django_south_version: ''
114+ vcs: 'git'
115+ repos_url: 'https://github.com/bartTC/dpaste.git'
116+ repos_branch: '2.6'
117+ application_path: 'dpaste'
118+ django_settings: 'dpaste.settings'
119+ settings_injection_path: 'settings/__init__.py'
120+ urls_injection_path: 'urls/__init__.py'
121+ requirements_pip_files: 'requirements.txt'
122+ additional_distro_packages: "python-imaging,python-tz,python-dev,build-essential,libpq-dev,libmysqlclient-dev,libxml2-dev,libxslt1-dev"
123+
124+
125 ## Upgrade the charm
126
127 This charm allow you to upgrade your deployment using Juju's `upgrade-charm`
128@@ -174,6 +182,10 @@
129
130 If you want to extend the fabfile check out [fabtools](http://fabtools.readthedocs.org/).
131
132+## Ansible
133+
134+See: https://github.com/cmars/juju-ansible
135+
136 ## Security
137
138 Note that if you're using a `requirement.txt` file the packages will
139@@ -207,6 +219,64 @@
140
141 ## Changelog
142
143+### X: Not yet released
144+
145+- Ansible only rewrite
146+- Python3 compatibility
147+- Support for Django 1.7
148+- Support for virtualenv
149+- Tests
150+- More vcs options
151+- More pip options
152+- All Juju's generated django config files have there priority set to 60
153+
154+New options:
155+
156+- wsgi_log_dir
157+
158+Backwards incompatible changes:
159+
160+- unit-config was renamed unit_config
161+- django_allowed_hosts needs commas as separators instead of spaces
162+- rename repos_dir for vcs_clone_dir because of a name collision with juju
163+
164+### 6: Notable changes:
165+
166+- The charm now use charm-helpers for hooks
167+- The chams now use ansible for installation
168+- Mysql and Redis and rabbitmq (celery) support was added
169+- The charm can install South and do migrations
170+- The charm include a Fabric and Ansible scripts to interact with Juju
171+
172+Note that I still need to think about the role of Fabric, Ansible, Salt, charm-helpers, etc
173+
174+Configuration changes:
175+
176+- python_path now accept multiple path separated by commas
177+- requirements_pip_files now also accept urls
178+- requirements_pip_files and requirements_apt_files are empty by default
179+- settings_secret_key_path was renamed settings_secret_key_name
180+- settings_database_path was renamed settings_database_name
181+
182+New options:
183+
184+- unit-config
185+- settings_amqp_name
186+- celery_always_eager
187+- django_south
188+- django_south_version
189+- django_debug
190+- django_allowed_host
191+- django_extra_settings
192+- settings_injection_path
193+- url_injection_path
194+- pip_extra_args
195+
196+Backwards incompatible changes:
197+
198+- Some default options changed so be sure to checkout your configuration files.
199+- The use of Ansible change a bit how things are done now be sure to test.
200+
201 ### 3: Notable changes:
202
203 - Rewrite the charm using python instead of BASH scripts
204
205=== added file 'ansible.py'
206--- ansible.py 1970-01-01 00:00:00 +0000
207+++ ansible.py 2014-07-07 20:28:28 +0000
208@@ -0,0 +1,30 @@
209+#!/usr/bin/env python
210+# encoding: utf-8
211+
212+import sys
213+from subprocess import Popen, PIPE
214+
215+import yaml
216+import json
217+
218+
219+d = yaml.safe_load(Popen(['juju','status'],stdout=PIPE).stdout)
220+
221+services = d.get("services", {})
222+if services is None:
223+ sys.exit(0)
224+
225+groups = {}
226+for service in services.items():
227+ if service is None:
228+ continue
229+
230+ units = services.get(service[0], {}).get("units", {})
231+ if units is None:
232+ continue
233+
234+ for unit in units.items():
235+ if 'public-address' in unit[1].keys():
236+ groups.setdefault(service[0], {"hosts" :[]})['hosts'].append(unit[1]['public-address'])
237+
238+print json.dumps(groups)
239
240=== added directory 'bin'
241=== added file 'bin/charm_helpers_sync.py'
242--- bin/charm_helpers_sync.py 1970-01-01 00:00:00 +0000
243+++ bin/charm_helpers_sync.py 2014-07-07 20:28:28 +0000
244@@ -0,0 +1,225 @@
245+#!/usr/bin/python
246+#
247+# Copyright 2013 Canonical Ltd.
248+
249+# Authors:
250+# Adam Gandelman <adamg@ubuntu.com>
251+#
252+
253+import logging
254+import optparse
255+import os
256+import subprocess
257+import shutil
258+import sys
259+import tempfile
260+import yaml
261+
262+from fnmatch import fnmatch
263+
264+CHARM_HELPERS_BRANCH = 'lp:charm-helpers'
265+
266+
267+def parse_config(conf_file):
268+ if not os.path.isfile(conf_file):
269+ logging.error('Invalid config file: %s.' % conf_file)
270+ return False
271+ return yaml.load(open(conf_file).read())
272+
273+
274+def clone_helpers(work_dir, branch):
275+ dest = os.path.join(work_dir, 'charm-helpers')
276+ logging.info('Checking out %s to %s.' % (branch, dest))
277+ cmd = ['bzr', 'branch', branch, dest]
278+ subprocess.check_call(cmd)
279+ return dest
280+
281+
282+def _module_path(module):
283+ return os.path.join(*module.split('.'))
284+
285+
286+def _src_path(src, module):
287+ return os.path.join(src, 'charmhelpers', _module_path(module))
288+
289+
290+def _dest_path(dest, module):
291+ return os.path.join(dest, _module_path(module))
292+
293+
294+def _is_pyfile(path):
295+ return os.path.isfile(path + '.py')
296+
297+
298+def ensure_init(path):
299+ '''
300+ ensure directories leading up to path are importable, omitting
301+ parent directory, eg path='/hooks/helpers/foo'/:
302+ hooks/
303+ hooks/helpers/__init__.py
304+ hooks/helpers/foo/__init__.py
305+ '''
306+ for d, dirs, files in os.walk(os.path.join(*path.split('/')[:2])):
307+ _i = os.path.join(d, '__init__.py')
308+ if not os.path.exists(_i):
309+ logging.info('Adding missing __init__.py: %s' % _i)
310+ open(_i, 'wb').close()
311+
312+
313+def sync_pyfile(src, dest):
314+ src = src + '.py'
315+ src_dir = os.path.dirname(src)
316+ logging.info('Syncing pyfile: %s -> %s.' % (src, dest))
317+ if not os.path.exists(dest):
318+ os.makedirs(dest)
319+ shutil.copy(src, dest)
320+ if os.path.isfile(os.path.join(src_dir, '__init__.py')):
321+ shutil.copy(os.path.join(src_dir, '__init__.py'),
322+ dest)
323+ ensure_init(dest)
324+
325+
326+def get_filter(opts=None):
327+ opts = opts or []
328+ if 'inc=*' in opts:
329+ # do not filter any files, include everything
330+ return None
331+
332+ def _filter(dir, ls):
333+ incs = [opt.split('=').pop() for opt in opts if 'inc=' in opt]
334+ _filter = []
335+ for f in ls:
336+ _f = os.path.join(dir, f)
337+
338+ if not os.path.isdir(_f) and not _f.endswith('.py') and incs:
339+ if True not in [fnmatch(_f, inc) for inc in incs]:
340+ logging.debug('Not syncing %s, does not match include '
341+ 'filters (%s)' % (_f, incs))
342+ _filter.append(f)
343+ else:
344+ logging.debug('Including file, which matches include '
345+ 'filters (%s): %s' % (incs, _f))
346+ elif (os.path.isfile(_f) and not _f.endswith('.py')):
347+ logging.debug('Not syncing file: %s' % f)
348+ _filter.append(f)
349+ elif (os.path.isdir(_f) and not
350+ os.path.isfile(os.path.join(_f, '__init__.py'))):
351+ logging.debug('Not syncing directory: %s' % f)
352+ _filter.append(f)
353+ return _filter
354+ return _filter
355+
356+
357+def sync_directory(src, dest, opts=None):
358+ if os.path.exists(dest):
359+ logging.debug('Removing existing directory: %s' % dest)
360+ shutil.rmtree(dest)
361+ logging.info('Syncing directory: %s -> %s.' % (src, dest))
362+
363+ shutil.copytree(src, dest, ignore=get_filter(opts))
364+ ensure_init(dest)
365+
366+
367+def sync(src, dest, module, opts=None):
368+ if os.path.isdir(_src_path(src, module)):
369+ sync_directory(_src_path(src, module), _dest_path(dest, module), opts)
370+ elif _is_pyfile(_src_path(src, module)):
371+ sync_pyfile(_src_path(src, module),
372+ os.path.dirname(_dest_path(dest, module)))
373+ else:
374+ logging.warn('Could not sync: %s. Neither a pyfile or directory, '
375+ 'does it even exist?' % module)
376+
377+
378+def parse_sync_options(options):
379+ if not options:
380+ return []
381+ return options.split(',')
382+
383+
384+def extract_options(inc, global_options=None):
385+ global_options = global_options or []
386+ if global_options and isinstance(global_options, basestring):
387+ global_options = [global_options]
388+ if '|' not in inc:
389+ return (inc, global_options)
390+ inc, opts = inc.split('|')
391+ return (inc, parse_sync_options(opts) + global_options)
392+
393+
394+def sync_helpers(include, src, dest, options=None):
395+ if not os.path.isdir(dest):
396+ os.mkdir(dest)
397+
398+ global_options = parse_sync_options(options)
399+
400+ for inc in include:
401+ if isinstance(inc, str):
402+ inc, opts = extract_options(inc, global_options)
403+ sync(src, dest, inc, opts)
404+ elif isinstance(inc, dict):
405+ # could also do nested dicts here.
406+ for k, v in inc.iteritems():
407+ if isinstance(v, list):
408+ for m in v:
409+ inc, opts = extract_options(m, global_options)
410+ sync(src, dest, '%s.%s' % (k, inc), opts)
411+
412+if __name__ == '__main__':
413+ parser = optparse.OptionParser()
414+ parser.add_option('-c', '--config', action='store', dest='config',
415+ default=None, help='helper config file')
416+ parser.add_option('-D', '--debug', action='store_true', dest='debug',
417+ default=False, help='debug')
418+ parser.add_option('-b', '--branch', action='store', dest='branch',
419+ help='charm-helpers bzr branch (overrides config)')
420+ parser.add_option('-d', '--destination', action='store', dest='dest_dir',
421+ help='sync destination dir (overrides config)')
422+ (opts, args) = parser.parse_args()
423+
424+ if opts.debug:
425+ logging.basicConfig(level=logging.DEBUG)
426+ else:
427+ logging.basicConfig(level=logging.INFO)
428+
429+ if opts.config:
430+ logging.info('Loading charm helper config from %s.' % opts.config)
431+ config = parse_config(opts.config)
432+ if not config:
433+ logging.error('Could not parse config from %s.' % opts.config)
434+ sys.exit(1)
435+ else:
436+ config = {}
437+
438+ if 'branch' not in config:
439+ config['branch'] = CHARM_HELPERS_BRANCH
440+ if opts.branch:
441+ config['branch'] = opts.branch
442+ if opts.dest_dir:
443+ config['destination'] = opts.dest_dir
444+
445+ if 'destination' not in config:
446+ logging.error('No destination dir. specified as option or config.')
447+ sys.exit(1)
448+
449+ if 'include' not in config:
450+ if not args:
451+ logging.error('No modules to sync specified as option or config.')
452+ sys.exit(1)
453+ config['include'] = []
454+ [config['include'].append(a) for a in args]
455+
456+ sync_options = None
457+ if 'options' in config:
458+ sync_options = config['options']
459+ tmpd = tempfile.mkdtemp()
460+ try:
461+ checkout = clone_helpers(tmpd, config['branch'])
462+ sync_helpers(config['include'], checkout, config['destination'],
463+ options=sync_options)
464+ except Exception, e:
465+ logging.error("Could not sync: %s" % e)
466+ raise e
467+ finally:
468+ logging.debug('Cleaning up %s' % tmpd)
469+ shutil.rmtree(tmpd)
470
471=== added file 'charm-helpers.yaml'
472--- charm-helpers.yaml 1970-01-01 00:00:00 +0000
473+++ charm-helpers.yaml 2014-07-07 20:28:28 +0000
474@@ -0,0 +1,7 @@
475+branch: lp:charm-helpers
476+destination: hooks/charmhelpers
477+include:
478+ - core
479+ - fetch
480+ - contrib.ansible|inc=*
481+ - contrib.templating.contexts
482
483=== modified file 'config.yaml'
484--- config.yaml 2013-05-31 15:04:04 +0000
485+++ config.yaml 2014-07-07 20:28:28 +0000
486@@ -6,6 +6,12 @@
487 The web site secret key. Leave empty will generate one.
488 NOTE: You **NEED** to set this in a multi-units architecture or you will
489 have some trouble.
490+ django_uid:
491+ type: string
492+ default: "www-data"
493+ django_gid:
494+ type: string
495+ default: "www-data"
496 django_version:
497 type: string
498 default: "distro"
499@@ -13,6 +19,7 @@
500 Version or origin from which to install. May be one of the following:
501 distro (default), ppa:somecustom/ppa, a deb url sources entry or
502 a valid pip line like 'Django' or 'Django==1.5' or a reposiroty url (without the -e).
503+ Leaving it empty if you don't want the charm to install Django.
504 vcs:
505 type: string
506 default: ""
507@@ -73,6 +80,14 @@
508 description: |
509 The relative path to install_root where the manage.py
510 script is located.
511+ unit_config:
512+ type: string
513+ default: ""
514+ description: |
515+ base64 encoded string to hold configuration information for the unit.
516+ The contents will be written to a file named
517+ <install_root>/<unit>/unit_config
518+ where <unit> is the location the branch is extracted to.
519 additional_distro_packages:
520 type: string
521 default: "python-imaging,python-docutils,python-tz"
522@@ -85,47 +100,109 @@
523 Comma separated extra packages to install.
524 requirements_pip_files:
525 type: string
526- default: "requirements.txt,requirements.pip"
527+ default: ""
528 description: |
529- Comma separated relative paths to requirement files. Note that the charm
530+ Comma separated relative paths or urls to a requirement files. Note that the charm
531 won't manually upgrade packages defined in this file.
532- Set the variable to an empty string if you don't want the feature.
533+ Leave the variable to an empty string if you don't want the feature.
534+ pip_extra_args:
535+ type: string
536+ default: ""
537+ description: |
538+ Extra arguments passed to pip.
539 requirements_apt_files:
540 type: string
541- default: "requirements.apt"
542+ default: ""
543 description: |
544 Comma separated relative paths to requirement files. Note that the charm
545 won't manually upgrade packages defined in this file.
546- Set the variable to an empty string if you don't want the feature.
547+ Leave the variable to an empty string if you don't want the feature.
548 django_settings:
549 type: string
550- default: "settings"
551+ default: ""
552 description: |
553 The Python path to your Django settings module.
554+ Leave it empty if your settings file is at the root of your repos.
555+ django_south:
556+ type: boolean
557+ default: False
558+ description: |
559+ Enable the use of south migrations
560+ django_south_version:
561+ type: string
562+ default: "distro"
563+ description: |
564+ Version or origin from which to install. May be one of the following:
565+ distro (default), ppa:somecustom/ppa, a deb url sources entry or
566+ a valid pip line like 'South' or 'South==0.8.4' or a reposiroty url (without the -e).
567+ Leaving it empty if you don't want the charm to install South.
568+ django_debug:
569+ type: boolean
570+ default: False
571+ description: |
572+ Enable disable settings.DEBUG for django
573+ django_allowed_hosts:
574+ type: string
575+ default: ""
576+ description: |
577+ A comma separated list for settings.ALLOWED_HOSTS in django. Default
578+ value will be the hostname, fully-qualified name, and public IP.
579+ django_extra_settings:
580+ type: string
581+ default: ""
582+ description: |
583+ Allows setting up extra settings.* values for Django. Acceptable
584+ values are limited to comma delimited key-value pairs like:
585+ SETTING_X=foo, SETTING_Y=bar
586 urls_dir_name:
587 type: string
588 default: "juju_urls"
589 description: |
590- The place where the generated urls will be written.
591+ The name of the directory where generated url will be written.
592 Set the variable to an empty string if you don't want the feature.
593+ urls_injection_path:
594+ type: string
595+ default: "urls.py"
596+ description: |
597+ The place where the code injection path will be append.
598+ This is relative to the urls_dir_name path define earlier.
599 settings_dir_name:
600 type: string
601 default: "juju_settings"
602 description: |
603- The place where the generated settings will be written.
604+ The name of the directory where generated settings will be written.
605 Set the variable to an empty string if you don't want the feature.
606- settings_database_path:
607- type: string
608- default: "juju_settings/20-engine-%(engine_name)s.py"
609+ settings_injection_path:
610+ type: string
611+ default: "settings.py"
612+ description: |
613+ The place where the code injection path will be append.
614+ This is relative to the settings_dir_name path define earlier.
615+ settings_database_name:
616+ type: string
617+ default: "60-engine-%(engine_name)s.py"
618 description: |
619 The place where the database configuration will be appended or written.
620 Set the variable to an empty string if you don't want the feature.
621- settings_secret_key_path:
622+ settings_secret_key_name:
623 type: string
624- default: "juju_settings/10-secret.py"
625+ default: "60-secret.py"
626 description: |
627 The place where the secret key configuration will be appended or written.
628 Set the variable to an empty string if you don't want the feature.
629+ settings_amqp_name:
630+ type: string
631+ default: "60-amqp.py"
632+ description: |
633+ The place where the amqp configuration will be appended or written.
634+ celery_always_eager:
635+ type: string
636+ default: "False"
637+ description: If True, all tasks will be executed locally by blocking until the task returns.
638+ celery_amqp_vhost:
639+ type: string
640+ default: ""
641+ description: Set a specific vhost for amqp relation. By default it's the unit_name.
642 wsgi_wsgi_file:
643 type: string
644 default: "wsgi"
645@@ -136,11 +213,11 @@
646 description: "The number of worker process for handling requests. 0 for count(cpu) + 1"
647 wsgi_worker_class:
648 type: string
649- default: "sync"
650- description: "Gunicorn workers type. Can be: sync, eventlet, gevent, tornado"
651+ default: ""
652+ description: "Socket protocol. Can be: http (default), uwsgi, fastcgi or workers type. Can be: sync (default), eventlet, gevent, tornado"
653 wsgi_worker_connections:
654 type: int
655- default: 1000
656+ default: 128
657 description: "The maximum number of simultaneous clients."
658 wsgi_max_requests:
659 type: int
660@@ -160,8 +237,8 @@
661 description: "Keep alive time in seconds."
662 wsgi_umask:
663 type: string
664- default: "0"
665- description: "A bit mask for the file mode on files written by Gunicorn. The number 0 means Python guesses the base. Note that this affects unix socket permissions."
666+ default: "0002"
667+ description: "A bit mask for the file mode on files written."
668 wsgi_user:
669 type: string
670 default: "www-data"
671@@ -172,7 +249,7 @@
672 description: "Switch worker process to run as this group. A valid group id (as an int) or the name."
673 wsgi_log_file:
674 type: string
675- default: "-"
676+ default: ""
677 description: "The log file to write to. If empty the logs would be handle by upstart."
678 wsgi_log_level:
679 type: string
680@@ -193,15 +270,15 @@
681 wsgi_timestamp:
682 type: string
683 default: ""
684- description: "The variable to modify to trigger Gunicorn reload."
685+ description: "The variable to modify to trigger reloads."
686 python_path:
687 type: string
688 default: ""
689- description: "Set an additionnal PYTHONPATH to the project."
690+ description: "Set additionnals, colon separated, PYTHONPATH to the project."
691 listen_ip:
692 type: string
693 default: "0.0.0.0"
694- description: "IP adresses that Gunicorn will listen on. By default we listen on all of them."
695+ description: "IP adresses to listen on. By default we listen on all of them."
696 port:
697 type: int
698 default: 8080
699
700=== added directory 'dev'
701=== added file 'dev/ubuntu-deps'
702--- dev/ubuntu-deps 1970-01-01 00:00:00 +0000
703+++ dev/ubuntu-deps 2014-07-07 20:28:28 +0000
704@@ -0,0 +1,17 @@
705+#!/bin/bash -e
706+# Needs to be run as a user who can sudo. d'oh!
707+# It will ask your password a lot.
708+
709+# Install add-apt-repository (packaging differs across releases).
710+lsb_release -r | grep -q 12.04 \
711+ && sudo apt-get -y install python-software-properties \
712+ || sudo apt-get -y install software-properties-common
713+
714+# Add the juju stable ppa, install charm-tools (juju-test plugin) and other deps
715+sudo add-apt-repository -y ppa:juju/stable
716+sudo apt-get update
717+sudo apt-get -y install juju-deployer juju-core charm-tools python3 python3-yaml
718+
719+# python3-flake8 was introduced after 12.04. Releases prior to that are not
720+# supported.
721+lsb_release -r | grep -q 12.04 || sudo apt-get -y install python3-flake8
722
723=== modified file 'fabfile.py'
724--- fabfile.py 2013-05-29 18:52:19 +0000
725+++ fabfile.py 2014-07-07 20:28:28 +0000
726@@ -7,8 +7,14 @@
727 import yaml
728
729 from fabric.api import env, run, sudo, task, put
730-from fabric.context_managers import cd
731+from fabric.context_managers import cd, shell_env
732 from fabric.contrib import files
733+from fabric.state import output
734+
735+# Be quiet by default
736+env.output_prefix = ''
737+output['running'] = False
738+output['status'] = False
739
740
741 # helpers
742@@ -24,8 +30,8 @@
743 def _config_get(service_name):
744 yaml_conf = Popen(['juju', 'get', service_name], stdout=PIPE)
745 conf = yaml.safe_load(yaml_conf.stdout)
746- orig_conf = yaml.safe_load(open('config.yaml', 'r'))['options']
747- return {k: (v['value'] if v['value'] is not None else orig_conf[k]['default']) for k,v in conf['settings'].iteritems()}
748+ return {k: (v['value'] if 'value' in v else v['default']) for k,v in conf['settings'].iteritems()}
749+
750
751 def _find_django_admin_cmd():
752 for cmd in ['django-admin.py', 'django-admin']:
753@@ -39,7 +45,7 @@
754 # Initialisation
755 env.user = 'ubuntu'
756
757-d = yaml.safe_load(Popen(['juju','status'],stdout=PIPE).stdout)
758+d = yaml.safe_load(Popen(['juju', 'status'], stdout=PIPE).stdout)
759
760 services = d.get("services", {})
761 if services is None:
762@@ -59,12 +65,29 @@
763 env.roledefs.setdefault(service[0], []).append(unit[1]['public-address'])
764 env.roledefs.setdefault(unit[0], []).append(unit[1]['public-address'])
765
766+if not env.roles:
767+ print "You must select a charm or a unit with the -R option to perform a task"
768+ print "Charms: %s" % [role for role in env.roledefs.keys() if not '/' in role]
769+ print "Units: %s" % [host for host in env.roledefs.keys() if '/' in host]
770+ print
771+else:
772+ env.service_name = env.roles[0].split('/')[0]
773+ env.sanitized_service_name = _sanitize(env.service_name)
774+ env.conf = _config_get(env.service_name)
775+ if not env.conf['django_settings']:
776+ django_settings_modules = '.'.join([env.sanitized_service_name, 'settings'])
777+ if env.conf['application_path']:
778+ django_settings_modules = '.'.join([os.path.basename(env.conf['application_path']),
779+ 'settings'])
780+ else:
781+ django_settings_modules = env.conf['django_settings']
782
783-env.service_name = env.roles[0].split('/')[0]
784-env.sanitized_service_name = _sanitize(env.service_name)
785-env.conf = _config_get(env.service_name)
786-env.project_dir = os.path.join(env.conf['install_root'], env.sanitized_service_name)
787-env.django_settings_modules = '.'.join([env.sanitized_service_name, env.conf['django_settings']])
788+ env.project_dir = os.path.join(env.conf['install_root'], env.sanitized_service_name)
789+ env.site_path = os.path.join(env.project_dir, env.conf['application_path'])
790+ if env.conf['python_path']:
791+ env.python_path = ':'.join([env.conf['install_root'], env.conf['python_path'].replace(',', ':')])
792+ else:
793+ env.python_path = env.conf['install_root']
794
795
796 # Debian
797@@ -75,6 +98,7 @@
798 """
799 sudo('apt-get install -y %s' % packages)
800
801+
802 @task
803 def apt_update():
804 """
805@@ -82,6 +106,7 @@
806 """
807 sudo('apt-get update')
808
809+
810 @task
811 def apt_dist_upgrade():
812 """
813@@ -89,6 +114,7 @@
814 """
815 sudo('apt-get dist-upgrade -y')
816
817+
818 @task
819 def apt_install_r():
820 """
821@@ -98,7 +124,8 @@
822 for req_file in env.conf['requirements_apt_files'].split(','):
823 sudo("apt-get install -y $(cat %s | tr '\\n' ' '" % req_file)
824
825-# Python
826+
827+# Pip
828 @task
829 def pip_install(packages):
830 """
831@@ -106,6 +133,7 @@
832 """
833 sudo("pip install %s" % packages)
834
835+
836 @task
837 def pip_install_r():
838 """
839@@ -115,6 +143,15 @@
840 for req_file in env.conf['requirements_pip_files'].split(','):
841 sudo("pip install -r %s" % req_file)
842
843+
844+@task
845+def pip_freeze():
846+ """
847+ List installed python packages
848+ """
849+ sudo("pip freeze")
850+
851+
852 # Users
853 @task
854 def adduser(username):
855@@ -123,22 +160,6 @@
856 """
857 sudo('adduser %s --disabled-password --gecos ""' % username)
858
859-@task
860-def ssh_add_key(pub_key_file, username=None):
861- """
862- Add a public SSH key to the authorized_keys file on the remote machine.
863- """
864- with open(os.path.normpath(pub_key_file), 'rt') as f:
865- ssh_key = f.read()
866-
867- if username is None:
868- run('mkdir -p .ssh')
869- files.append('.ssh/authorized_keys', ssh_key)
870- else:
871- run('mkdir -p /home/%s/.ssh' % username)
872- files.append('/home/%s/.ssh/authorized_keys' % username, ssh_key)
873- run('chown -R %s:%s /home/%s/.ssh' % (username, username, username))
874-
875
876 # VCS
877
878@@ -175,9 +196,13 @@
879 def manage(command):
880 """ Runs management commands."""
881 django_admin_cmd = _find_django_admin_cmd()
882- sudo('%s %s --pythonpath=%s --settings=%s' % \
883- (django_admin_cmd, command, env.conf['install_root'], env.django_settings_modules), \
884- user=env.conf['wsgi_user'])
885+
886+ with cd(env.site_path):
887+ with shell_env(PYTHONPATH=':'.join([os.path.join(env.site_path, '../'), env.python_path])):
888+ sudo('%s %s --settings=%s' %
889+ (django_admin_cmd, command, django_settings_modules),
890+ user=env.conf['wsgi_user'])
891+
892
893 @task
894 def load_fixture(fixture_path):
895@@ -187,11 +212,11 @@
896 manage('loaddata %s' % os.path.join('/tmp/', fixture_file))
897 run('rm %s' % os.path.join('/tmp/', fixture_file))
898
899+
900 # Utils
901 @task
902 def delete_pyc():
903 """ Deletes *.pyc files from project source dir """
904-
905- with env.project_dir:
906- run("find . -name '*.pyc' -delete")
907-
908+ with cd(env.project_dir):
909+ sudo("find . -name '*.pyc' -delete", user="www-data")
910+ reload()
911
912=== added symlink 'hooks/amqp-relation-broken'
913=== target is u'hooks.py'
914=== added symlink 'hooks/amqp-relation-changed'
915=== target is u'hooks.py'
916=== added symlink 'hooks/amqp-relation-joined'
917=== target is u'hooks.py'
918=== added directory 'hooks/charmhelpers'
919=== added file 'hooks/charmhelpers/__init__.py'
920=== added directory 'hooks/charmhelpers/contrib'
921=== added file 'hooks/charmhelpers/contrib/__init__.py'
922=== added directory 'hooks/charmhelpers/contrib/ansible'
923=== added file 'hooks/charmhelpers/contrib/ansible/__init__.py'
924--- hooks/charmhelpers/contrib/ansible/__init__.py 1970-01-01 00:00:00 +0000
925+++ hooks/charmhelpers/contrib/ansible/__init__.py 2014-07-07 20:28:28 +0000
926@@ -0,0 +1,165 @@
927+# Copyright 2013 Canonical Ltd.
928+#
929+# Authors:
930+# Charm Helpers Developers <juju@lists.ubuntu.com>
931+"""Charm Helpers ansible - declare the state of your machines.
932+
933+This helper enables you to declare your machine state, rather than
934+program it procedurally (and have to test each change to your procedures).
935+Your install hook can be as simple as:
936+
937+{{{
938+import charmhelpers.contrib.ansible
939+
940+
941+def install():
942+ charmhelpers.contrib.ansible.install_ansible_support()
943+ charmhelpers.contrib.ansible.apply_playbook('playbooks/install.yaml')
944+}}}
945+
946+and won't need to change (nor will its tests) when you change the machine
947+state.
948+
949+All of your juju config and relation-data are available as template
950+variables within your playbooks and templates. An install playbook looks
951+something like:
952+
953+{{{
954+---
955+- hosts: localhost
956+ user: root
957+
958+ tasks:
959+ - name: Add private repositories.
960+ template:
961+ src: ../templates/private-repositories.list.jinja2
962+ dest: /etc/apt/sources.list.d/private.list
963+
964+ - name: Update the cache.
965+ apt: update_cache=yes
966+
967+ - name: Install dependencies.
968+ apt: pkg={{ item }}
969+ with_items:
970+ - python-mimeparse
971+ - python-webob
972+ - sunburnt
973+
974+ - name: Setup groups.
975+ group: name={{ item.name }} gid={{ item.gid }}
976+ with_items:
977+ - { name: 'deploy_user', gid: 1800 }
978+ - { name: 'service_user', gid: 1500 }
979+
980+ ...
981+}}}
982+
983+Read more online about playbooks[1] and standard ansible modules[2].
984+
985+[1] http://www.ansibleworks.com/docs/playbooks.html
986+[2] http://www.ansibleworks.com/docs/modules.html
987+"""
988+import os
989+import subprocess
990+
991+import charmhelpers.contrib.templating.contexts
992+import charmhelpers.core.host
993+import charmhelpers.core.hookenv
994+import charmhelpers.fetch
995+
996+
997+charm_dir = os.environ.get('CHARM_DIR', '')
998+ansible_hosts_path = '/etc/ansible/hosts'
999+# Ansible will automatically include any vars in the following
1000+# file in its inventory when run locally.
1001+ansible_vars_path = '/etc/ansible/host_vars/localhost'
1002+
1003+
1004+def install_ansible_support(from_ppa=True):
1005+ """Installs the ansible package.
1006+
1007+ By default it is installed from the PPA [1] linked from
1008+ the ansible website [2].
1009+
1010+ [1] https://launchpad.net/~rquillo/+archive/ansible
1011+ [2] http://www.ansibleworks.com/docs/gettingstarted.html#ubuntu-and-debian
1012+
1013+ If from_ppa is false, you must ensure that the package is available
1014+ from a configured repository.
1015+ """
1016+ if from_ppa:
1017+ charmhelpers.fetch.add_source('ppa:rquillo/ansible')
1018+ charmhelpers.fetch.apt_update(fatal=True)
1019+ charmhelpers.fetch.apt_install('ansible')
1020+ with open(ansible_hosts_path, 'w+') as hosts_file:
1021+ hosts_file.write('localhost ansible_connection=local')
1022+
1023+
1024+def apply_playbook(playbook, tags=None):
1025+ tags = tags or []
1026+ tags = ",".join(tags)
1027+ charmhelpers.contrib.templating.contexts.juju_state_to_yaml(
1028+ ansible_vars_path, namespace_separator='__',
1029+ allow_hyphens_in_keys=False)
1030+ call = [
1031+ 'ansible-playbook',
1032+ '-c',
1033+ 'local',
1034+ playbook,
1035+ ]
1036+ if tags:
1037+ call.extend(['--tags', '{}'.format(tags)])
1038+ subprocess.check_call(call)
1039+
1040+
1041+class AnsibleHooks(charmhelpers.core.hookenv.Hooks):
1042+ """Run a playbook with the hook-name as the tag.
1043+
1044+ This helper builds on the standard hookenv.Hooks helper,
1045+ but additionally runs the playbook with the hook-name specified
1046+ using --tags (ie. running all the tasks tagged with the hook-name).
1047+
1048+ Example:
1049+ hooks = AnsibleHooks(playbook_path='playbooks/my_machine_state.yaml')
1050+
1051+ # All the tasks within my_machine_state.yaml tagged with 'install'
1052+ # will be run automatically after do_custom_work()
1053+ @hooks.hook()
1054+ def install():
1055+ do_custom_work()
1056+
1057+ # For most of your hooks, you won't need to do anything other
1058+ # than run the tagged tasks for the hook:
1059+ @hooks.hook('config-changed', 'start', 'stop')
1060+ def just_use_playbook():
1061+ pass
1062+
1063+ # As a convenience, you can avoid the above noop function by specifying
1064+ # the hooks which are handled by ansible-only and they'll be registered
1065+ # for you:
1066+ # hooks = AnsibleHooks(
1067+ # 'playbooks/my_machine_state.yaml',
1068+ # default_hooks=['config-changed', 'start', 'stop'])
1069+
1070+ if __name__ == "__main__":
1071+ # execute a hook based on the name the program is called by
1072+ hooks.execute(sys.argv)
1073+ """
1074+
1075+ def __init__(self, playbook_path, default_hooks=None):
1076+ """Register any hooks handled by ansible."""
1077+ super(AnsibleHooks, self).__init__()
1078+
1079+ self.playbook_path = playbook_path
1080+
1081+ default_hooks = default_hooks or []
1082+ noop = lambda *args, **kwargs: None
1083+ for hook in default_hooks:
1084+ self.register(hook, noop)
1085+
1086+ def execute(self, args):
1087+ """Execute the hook followed by the playbook using the hook as tag."""
1088+ super(AnsibleHooks, self).execute(args)
1089+ hook_name = os.path.basename(args[0])
1090+ charmhelpers.contrib.ansible.apply_playbook(
1091+ self.playbook_path, tags=[hook_name])
1092
1093=== added directory 'hooks/charmhelpers/contrib/templating'
1094=== added file 'hooks/charmhelpers/contrib/templating/__init__.py'
1095=== added file 'hooks/charmhelpers/contrib/templating/contexts.py'
1096--- hooks/charmhelpers/contrib/templating/contexts.py 1970-01-01 00:00:00 +0000
1097+++ hooks/charmhelpers/contrib/templating/contexts.py 2014-07-07 20:28:28 +0000
1098@@ -0,0 +1,104 @@
1099+# Copyright 2013 Canonical Ltd.
1100+#
1101+# Authors:
1102+# Charm Helpers Developers <juju@lists.ubuntu.com>
1103+"""A helper to create a yaml cache of config with namespaced relation data."""
1104+import os
1105+import yaml
1106+
1107+import charmhelpers.core.hookenv
1108+
1109+
1110+charm_dir = os.environ.get('CHARM_DIR', '')
1111+
1112+
1113+def dict_keys_without_hyphens(a_dict):
1114+ """Return the a new dict with underscores instead of hyphens in keys."""
1115+ return dict(
1116+ (key.replace('-', '_'), val) for key, val in a_dict.items())
1117+
1118+
1119+def update_relations(context, namespace_separator=':'):
1120+ """Update the context with the relation data."""
1121+ # Add any relation data prefixed with the relation type.
1122+ relation_type = charmhelpers.core.hookenv.relation_type()
1123+ relations = []
1124+ context['current_relation'] = {}
1125+ if relation_type is not None:
1126+ relation_data = charmhelpers.core.hookenv.relation_get()
1127+ context['current_relation'] = relation_data
1128+ # Deprecated: the following use of relation data as keys
1129+ # directly in the context will be removed.
1130+ relation_data = dict(
1131+ ("{relation_type}{namespace_separator}{key}".format(
1132+ relation_type=relation_type,
1133+ key=key,
1134+ namespace_separator=namespace_separator), val)
1135+ for key, val in relation_data.items())
1136+ relation_data = dict_keys_without_hyphens(relation_data)
1137+ context.update(relation_data)
1138+ relations = charmhelpers.core.hookenv.relations_of_type(relation_type)
1139+ relations = [dict_keys_without_hyphens(rel) for rel in relations]
1140+
1141+ if 'relations_deprecated' not in context:
1142+ context['relations_deprecated'] = {}
1143+ if relation_type is not None:
1144+ relation_type = relation_type.replace('-', '_')
1145+ context['relations_deprecated'][relation_type] = relations
1146+
1147+ context['relations'] = charmhelpers.core.hookenv.relations()
1148+
1149+
1150+def juju_state_to_yaml(yaml_path, namespace_separator=':',
1151+ allow_hyphens_in_keys=True):
1152+ """Update the juju config and state in a yaml file.
1153+
1154+ This includes any current relation-get data, and the charm
1155+ directory.
1156+
1157+ This function was created for the ansible and saltstack
1158+ support, as those libraries can use a yaml file to supply
1159+ context to templates, but it may be useful generally to
1160+ create and update an on-disk cache of all the config, including
1161+ previous relation data.
1162+
1163+ By default, hyphens are allowed in keys as this is supported
1164+ by yaml, but for tools like ansible, hyphens are not valid [1].
1165+
1166+ [1] http://www.ansibleworks.com/docs/playbooks_variables.html#what-makes-a-valid-variable-name
1167+ """
1168+ config = charmhelpers.core.hookenv.config()
1169+
1170+ # Add the charm_dir which we will need to refer to charm
1171+ # file resources etc.
1172+ config['charm_dir'] = charm_dir
1173+ config['local_unit'] = charmhelpers.core.hookenv.local_unit()
1174+ config['unit_private_address'] = charmhelpers.core.hookenv.unit_private_ip()
1175+ config['unit_public_address'] = charmhelpers.core.hookenv.unit_get(
1176+ 'public-address'
1177+ )
1178+
1179+ # Don't use non-standard tags for unicode which will not
1180+ # work when salt uses yaml.load_safe.
1181+ yaml.add_representer(unicode, lambda dumper,
1182+ value: dumper.represent_scalar(
1183+ u'tag:yaml.org,2002:str', value))
1184+
1185+ yaml_dir = os.path.dirname(yaml_path)
1186+ if not os.path.exists(yaml_dir):
1187+ os.makedirs(yaml_dir)
1188+
1189+ if os.path.exists(yaml_path):
1190+ with open(yaml_path, "r") as existing_vars_file:
1191+ existing_vars = yaml.load(existing_vars_file.read())
1192+ else:
1193+ existing_vars = {}
1194+
1195+ if not allow_hyphens_in_keys:
1196+ config = dict_keys_without_hyphens(config)
1197+ existing_vars.update(config)
1198+
1199+ update_relations(existing_vars, namespace_separator)
1200+
1201+ with open(yaml_path, "w+") as fp:
1202+ fp.write(yaml.dump(existing_vars, default_flow_style=False))
1203
1204=== added directory 'hooks/charmhelpers/core'
1205=== added file 'hooks/charmhelpers/core/__init__.py'
1206=== added file 'hooks/charmhelpers/core/fstab.py'
1207--- hooks/charmhelpers/core/fstab.py 1970-01-01 00:00:00 +0000
1208+++ hooks/charmhelpers/core/fstab.py 2014-07-07 20:28:28 +0000
1209@@ -0,0 +1,114 @@
1210+#!/usr/bin/env python
1211+# -*- coding: utf-8 -*-
1212+
1213+__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
1214+
1215+import os
1216+
1217+
1218+class Fstab(file):
1219+ """This class extends file in order to implement a file reader/writer
1220+ for file `/etc/fstab`
1221+ """
1222+
1223+ class Entry(object):
1224+ """Entry class represents a non-comment line on the `/etc/fstab` file
1225+ """
1226+ def __init__(self, device, mountpoint, filesystem,
1227+ options, d=0, p=0):
1228+ self.device = device
1229+ self.mountpoint = mountpoint
1230+ self.filesystem = filesystem
1231+
1232+ if not options:
1233+ options = "defaults"
1234+
1235+ self.options = options
1236+ self.d = d
1237+ self.p = p
1238+
1239+ def __eq__(self, o):
1240+ return str(self) == str(o)
1241+
1242+ def __str__(self):
1243+ return "{} {} {} {} {} {}".format(self.device,
1244+ self.mountpoint,
1245+ self.filesystem,
1246+ self.options,
1247+ self.d,
1248+ self.p)
1249+
1250+ DEFAULT_PATH = os.path.join(os.path.sep, 'etc', 'fstab')
1251+
1252+ def __init__(self, path=None):
1253+ if path:
1254+ self._path = path
1255+ else:
1256+ self._path = self.DEFAULT_PATH
1257+ file.__init__(self, self._path, 'r+')
1258+
1259+ def _hydrate_entry(self, line):
1260+ return Fstab.Entry(*filter(
1261+ lambda x: x not in ('', None),
1262+ line.strip("\n").split(" ")))
1263+
1264+ @property
1265+ def entries(self):
1266+ self.seek(0)
1267+ for line in self.readlines():
1268+ try:
1269+ if not line.startswith("#"):
1270+ yield self._hydrate_entry(line)
1271+ except ValueError:
1272+ pass
1273+
1274+ def get_entry_by_attr(self, attr, value):
1275+ for entry in self.entries:
1276+ e_attr = getattr(entry, attr)
1277+ if e_attr == value:
1278+ return entry
1279+ return None
1280+
1281+ def add_entry(self, entry):
1282+ if self.get_entry_by_attr('device', entry.device):
1283+ return False
1284+
1285+ self.write(str(entry) + '\n')
1286+ self.truncate()
1287+ return entry
1288+
1289+ def remove_entry(self, entry):
1290+ self.seek(0)
1291+
1292+ lines = self.readlines()
1293+
1294+ found = False
1295+ for index, line in enumerate(lines):
1296+ if not line.startswith("#"):
1297+ if self._hydrate_entry(line) == entry:
1298+ found = True
1299+ break
1300+
1301+ if not found:
1302+ return False
1303+
1304+ lines.remove(line)
1305+
1306+ self.seek(0)
1307+ self.write(''.join(lines))
1308+ self.truncate()
1309+ return True
1310+
1311+ @classmethod
1312+ def remove_by_mountpoint(cls, mountpoint, path=None):
1313+ fstab = cls(path=path)
1314+ entry = fstab.get_entry_by_attr('mountpoint', mountpoint)
1315+ if entry:
1316+ return fstab.remove_entry(entry)
1317+ return False
1318+
1319+ @classmethod
1320+ def add(cls, device, mountpoint, filesystem, options=None, path=None):
1321+ return cls(path=path).add_entry(Fstab.Entry(device,
1322+ mountpoint, filesystem,
1323+ options=options))
1324
1325=== added file 'hooks/charmhelpers/core/hookenv.py'
1326--- hooks/charmhelpers/core/hookenv.py 1970-01-01 00:00:00 +0000
1327+++ hooks/charmhelpers/core/hookenv.py 2014-07-07 20:28:28 +0000
1328@@ -0,0 +1,498 @@
1329+"Interactions with the Juju environment"
1330+# Copyright 2013 Canonical Ltd.
1331+#
1332+# Authors:
1333+# Charm Helpers Developers <juju@lists.ubuntu.com>
1334+
1335+import os
1336+import json
1337+import yaml
1338+import subprocess
1339+import sys
1340+import UserDict
1341+from subprocess import CalledProcessError
1342+
1343+CRITICAL = "CRITICAL"
1344+ERROR = "ERROR"
1345+WARNING = "WARNING"
1346+INFO = "INFO"
1347+DEBUG = "DEBUG"
1348+MARKER = object()
1349+
1350+cache = {}
1351+
1352+
1353+def cached(func):
1354+ """Cache return values for multiple executions of func + args
1355+
1356+ For example:
1357+
1358+ @cached
1359+ def unit_get(attribute):
1360+ pass
1361+
1362+ unit_get('test')
1363+
1364+ will cache the result of unit_get + 'test' for future calls.
1365+ """
1366+ def wrapper(*args, **kwargs):
1367+ global cache
1368+ key = str((func, args, kwargs))
1369+ try:
1370+ return cache[key]
1371+ except KeyError:
1372+ res = func(*args, **kwargs)
1373+ cache[key] = res
1374+ return res
1375+ return wrapper
1376+
1377+
1378+def flush(key):
1379+ """Flushes any entries from function cache where the
1380+ key is found in the function+args """
1381+ flush_list = []
1382+ for item in cache:
1383+ if key in item:
1384+ flush_list.append(item)
1385+ for item in flush_list:
1386+ del cache[item]
1387+
1388+
1389+def log(message, level=None):
1390+ """Write a message to the juju log"""
1391+ command = ['juju-log']
1392+ if level:
1393+ command += ['-l', level]
1394+ command += [message]
1395+ subprocess.call(command)
1396+
1397+
1398+class Serializable(UserDict.IterableUserDict):
1399+ """Wrapper, an object that can be serialized to yaml or json"""
1400+
1401+ def __init__(self, obj):
1402+ # wrap the object
1403+ UserDict.IterableUserDict.__init__(self)
1404+ self.data = obj
1405+
1406+ def __getattr__(self, attr):
1407+ # See if this object has attribute.
1408+ if attr in ("json", "yaml", "data"):
1409+ return self.__dict__[attr]
1410+ # Check for attribute in wrapped object.
1411+ got = getattr(self.data, attr, MARKER)
1412+ if got is not MARKER:
1413+ return got
1414+ # Proxy to the wrapped object via dict interface.
1415+ try:
1416+ return self.data[attr]
1417+ except KeyError:
1418+ raise AttributeError(attr)
1419+
1420+ def __getstate__(self):
1421+ # Pickle as a standard dictionary.
1422+ return self.data
1423+
1424+ def __setstate__(self, state):
1425+ # Unpickle into our wrapper.
1426+ self.data = state
1427+
1428+ def json(self):
1429+ """Serialize the object to json"""
1430+ return json.dumps(self.data)
1431+
1432+ def yaml(self):
1433+ """Serialize the object to yaml"""
1434+ return yaml.dump(self.data)
1435+
1436+
1437+def execution_environment():
1438+ """A convenient bundling of the current execution context"""
1439+ context = {}
1440+ context['conf'] = config()
1441+ if relation_id():
1442+ context['reltype'] = relation_type()
1443+ context['relid'] = relation_id()
1444+ context['rel'] = relation_get()
1445+ context['unit'] = local_unit()
1446+ context['rels'] = relations()
1447+ context['env'] = os.environ
1448+ return context
1449+
1450+
1451+def in_relation_hook():
1452+ """Determine whether we're running in a relation hook"""
1453+ return 'JUJU_RELATION' in os.environ
1454+
1455+
1456+def relation_type():
1457+ """The scope for the current relation hook"""
1458+ return os.environ.get('JUJU_RELATION', None)
1459+
1460+
1461+def relation_id():
1462+ """The relation ID for the current relation hook"""
1463+ return os.environ.get('JUJU_RELATION_ID', None)
1464+
1465+
1466+def local_unit():
1467+ """Local unit ID"""
1468+ return os.environ['JUJU_UNIT_NAME']
1469+
1470+
1471+def remote_unit():
1472+ """The remote unit for the current relation hook"""
1473+ return os.environ['JUJU_REMOTE_UNIT']
1474+
1475+
1476+def service_name():
1477+ """The name service group this unit belongs to"""
1478+ return local_unit().split('/')[0]
1479+
1480+
1481+def hook_name():
1482+ """The name of the currently executing hook"""
1483+ return os.path.basename(sys.argv[0])
1484+
1485+
1486+class Config(dict):
1487+ """A Juju charm config dictionary that can write itself to
1488+ disk (as json) and track which values have changed since
1489+ the previous hook invocation.
1490+
1491+ Do not instantiate this object directly - instead call
1492+ ``hookenv.config()``
1493+
1494+ Example usage::
1495+
1496+ >>> # inside a hook
1497+ >>> from charmhelpers.core import hookenv
1498+ >>> config = hookenv.config()
1499+ >>> config['foo']
1500+ 'bar'
1501+ >>> config['mykey'] = 'myval'
1502+ >>> config.save()
1503+
1504+
1505+ >>> # user runs `juju set mycharm foo=baz`
1506+ >>> # now we're inside subsequent config-changed hook
1507+ >>> config = hookenv.config()
1508+ >>> config['foo']
1509+ 'baz'
1510+ >>> # test to see if this val has changed since last hook
1511+ >>> config.changed('foo')
1512+ True
1513+ >>> # what was the previous value?
1514+ >>> config.previous('foo')
1515+ 'bar'
1516+ >>> # keys/values that we add are preserved across hooks
1517+ >>> config['mykey']
1518+ 'myval'
1519+ >>> # don't forget to save at the end of hook!
1520+ >>> config.save()
1521+
1522+ """
1523+ CONFIG_FILE_NAME = '.juju-persistent-config'
1524+
1525+ def __init__(self, *args, **kw):
1526+ super(Config, self).__init__(*args, **kw)
1527+ self._prev_dict = None
1528+ self.path = os.path.join(charm_dir(), Config.CONFIG_FILE_NAME)
1529+ if os.path.exists(self.path):
1530+ self.load_previous()
1531+
1532+ def load_previous(self, path=None):
1533+ """Load previous copy of config from disk so that current values
1534+ can be compared to previous values.
1535+
1536+ :param path:
1537+
1538+ File path from which to load the previous config. If `None`,
1539+ config is loaded from the default location. If `path` is
1540+ specified, subsequent `save()` calls will write to the same
1541+ path.
1542+
1543+ """
1544+ self.path = path or self.path
1545+ with open(self.path) as f:
1546+ self._prev_dict = json.load(f)
1547+
1548+ def changed(self, key):
1549+ """Return true if the value for this key has changed since
1550+ the last save.
1551+
1552+ """
1553+ if self._prev_dict is None:
1554+ return True
1555+ return self.previous(key) != self.get(key)
1556+
1557+ def previous(self, key):
1558+ """Return previous value for this key, or None if there
1559+ is no "previous" value.
1560+
1561+ """
1562+ if self._prev_dict:
1563+ return self._prev_dict.get(key)
1564+ return None
1565+
1566+ def save(self):
1567+ """Save this config to disk.
1568+
1569+ Preserves items in _prev_dict that do not exist in self.
1570+
1571+ """
1572+ if self._prev_dict:
1573+ for k, v in self._prev_dict.iteritems():
1574+ if k not in self:
1575+ self[k] = v
1576+ with open(self.path, 'w') as f:
1577+ json.dump(self, f)
1578+
1579+
1580+@cached
1581+def config(scope=None):
1582+ """Juju charm configuration"""
1583+ config_cmd_line = ['config-get']
1584+ if scope is not None:
1585+ config_cmd_line.append(scope)
1586+ config_cmd_line.append('--format=json')
1587+ try:
1588+ config_data = json.loads(subprocess.check_output(config_cmd_line))
1589+ if scope is not None:
1590+ return config_data
1591+ return Config(config_data)
1592+ except ValueError:
1593+ return None
1594+
1595+
1596+@cached
1597+def relation_get(attribute=None, unit=None, rid=None):
1598+ """Get relation information"""
1599+ _args = ['relation-get', '--format=json']
1600+ if rid:
1601+ _args.append('-r')
1602+ _args.append(rid)
1603+ _args.append(attribute or '-')
1604+ if unit:
1605+ _args.append(unit)
1606+ try:
1607+ return json.loads(subprocess.check_output(_args))
1608+ except ValueError:
1609+ return None
1610+ except CalledProcessError, e:
1611+ if e.returncode == 2:
1612+ return None
1613+ raise
1614+
1615+
1616+def relation_set(relation_id=None, relation_settings={}, **kwargs):
1617+ """Set relation information for the current unit"""
1618+ relation_cmd_line = ['relation-set']
1619+ if relation_id is not None:
1620+ relation_cmd_line.extend(('-r', relation_id))
1621+ for k, v in (relation_settings.items() + kwargs.items()):
1622+ if v is None:
1623+ relation_cmd_line.append('{}='.format(k))
1624+ else:
1625+ relation_cmd_line.append('{}={}'.format(k, v))
1626+ subprocess.check_call(relation_cmd_line)
1627+ # Flush cache of any relation-gets for local unit
1628+ flush(local_unit())
1629+
1630+
1631+@cached
1632+def relation_ids(reltype=None):
1633+ """A list of relation_ids"""
1634+ reltype = reltype or relation_type()
1635+ relid_cmd_line = ['relation-ids', '--format=json']
1636+ if reltype is not None:
1637+ relid_cmd_line.append(reltype)
1638+ return json.loads(subprocess.check_output(relid_cmd_line)) or []
1639+ return []
1640+
1641+
1642+@cached
1643+def related_units(relid=None):
1644+ """A list of related units"""
1645+ relid = relid or relation_id()
1646+ units_cmd_line = ['relation-list', '--format=json']
1647+ if relid is not None:
1648+ units_cmd_line.extend(('-r', relid))
1649+ return json.loads(subprocess.check_output(units_cmd_line)) or []
1650+
1651+
1652+@cached
1653+def relation_for_unit(unit=None, rid=None):
1654+ """Get the json represenation of a unit's relation"""
1655+ unit = unit or remote_unit()
1656+ relation = relation_get(unit=unit, rid=rid)
1657+ for key in relation:
1658+ if key.endswith('-list'):
1659+ relation[key] = relation[key].split()
1660+ relation['__unit__'] = unit
1661+ return relation
1662+
1663+
1664+@cached
1665+def relations_for_id(relid=None):
1666+ """Get relations of a specific relation ID"""
1667+ relation_data = []
1668+ relid = relid or relation_ids()
1669+ for unit in related_units(relid):
1670+ unit_data = relation_for_unit(unit, relid)
1671+ unit_data['__relid__'] = relid
1672+ relation_data.append(unit_data)
1673+ return relation_data
1674+
1675+
1676+@cached
1677+def relations_of_type(reltype=None):
1678+ """Get relations of a specific type"""
1679+ relation_data = []
1680+ reltype = reltype or relation_type()
1681+ for relid in relation_ids(reltype):
1682+ for relation in relations_for_id(relid):
1683+ relation['__relid__'] = relid
1684+ relation_data.append(relation)
1685+ return relation_data
1686+
1687+
1688+@cached
1689+def relation_types():
1690+ """Get a list of relation types supported by this charm"""
1691+ charmdir = os.environ.get('CHARM_DIR', '')
1692+ mdf = open(os.path.join(charmdir, 'metadata.yaml'))
1693+ md = yaml.safe_load(mdf)
1694+ rel_types = []
1695+ for key in ('provides', 'requires', 'peers'):
1696+ section = md.get(key)
1697+ if section:
1698+ rel_types.extend(section.keys())
1699+ mdf.close()
1700+ return rel_types
1701+
1702+
1703+@cached
1704+def relations():
1705+ """Get a nested dictionary of relation data for all related units"""
1706+ rels = {}
1707+ for reltype in relation_types():
1708+ relids = {}
1709+ for relid in relation_ids(reltype):
1710+ units = {local_unit(): relation_get(unit=local_unit(), rid=relid)}
1711+ for unit in related_units(relid):
1712+ reldata = relation_get(unit=unit, rid=relid)
1713+ units[unit] = reldata
1714+ relids[relid] = units
1715+ rels[reltype] = relids
1716+ return rels
1717+
1718+
1719+@cached
1720+def is_relation_made(relation, keys='private-address'):
1721+ '''
1722+ Determine whether a relation is established by checking for
1723+ presence of key(s). If a list of keys is provided, they
1724+ must all be present for the relation to be identified as made
1725+ '''
1726+ if isinstance(keys, str):
1727+ keys = [keys]
1728+ for r_id in relation_ids(relation):
1729+ for unit in related_units(r_id):
1730+ context = {}
1731+ for k in keys:
1732+ context[k] = relation_get(k, rid=r_id,
1733+ unit=unit)
1734+ if None not in context.values():
1735+ return True
1736+ return False
1737+
1738+
1739+def open_port(port, protocol="TCP"):
1740+ """Open a service network port"""
1741+ _args = ['open-port']
1742+ _args.append('{}/{}'.format(port, protocol))
1743+ subprocess.check_call(_args)
1744+
1745+
1746+def close_port(port, protocol="TCP"):
1747+ """Close a service network port"""
1748+ _args = ['close-port']
1749+ _args.append('{}/{}'.format(port, protocol))
1750+ subprocess.check_call(_args)
1751+
1752+
1753+@cached
1754+def unit_get(attribute):
1755+ """Get the unit ID for the remote unit"""
1756+ _args = ['unit-get', '--format=json', attribute]
1757+ try:
1758+ return json.loads(subprocess.check_output(_args))
1759+ except ValueError:
1760+ return None
1761+
1762+
1763+def unit_private_ip():
1764+ """Get this unit's private IP address"""
1765+ return unit_get('private-address')
1766+
1767+
1768+class UnregisteredHookError(Exception):
1769+ """Raised when an undefined hook is called"""
1770+ pass
1771+
1772+
1773+class Hooks(object):
1774+ """A convenient handler for hook functions.
1775+
1776+ Example:
1777+ hooks = Hooks()
1778+
1779+ # register a hook, taking its name from the function name
1780+ @hooks.hook()
1781+ def install():
1782+ ...
1783+
1784+ # register a hook, providing a custom hook name
1785+ @hooks.hook("config-changed")
1786+ def config_changed():
1787+ ...
1788+
1789+ if __name__ == "__main__":
1790+ # execute a hook based on the name the program is called by
1791+ hooks.execute(sys.argv)
1792+ """
1793+
1794+ def __init__(self):
1795+ super(Hooks, self).__init__()
1796+ self._hooks = {}
1797+
1798+ def register(self, name, function):
1799+ """Register a hook"""
1800+ self._hooks[name] = function
1801+
1802+ def execute(self, args):
1803+ """Execute a registered hook based on args[0]"""
1804+ hook_name = os.path.basename(args[0])
1805+ if hook_name in self._hooks:
1806+ self._hooks[hook_name]()
1807+ else:
1808+ raise UnregisteredHookError(hook_name)
1809+
1810+ def hook(self, *hook_names):
1811+ """Decorator, registering them as hooks"""
1812+ def wrapper(decorated):
1813+ for hook_name in hook_names:
1814+ self.register(hook_name, decorated)
1815+ else:
1816+ self.register(decorated.__name__, decorated)
1817+ if '_' in decorated.__name__:
1818+ self.register(
1819+ decorated.__name__.replace('_', '-'), decorated)
1820+ return decorated
1821+ return wrapper
1822+
1823+
1824+def charm_dir():
1825+ """Return the root directory of the current charm"""
1826+ return os.environ.get('CHARM_DIR')
1827
1828=== added file 'hooks/charmhelpers/core/host.py'
1829--- hooks/charmhelpers/core/host.py 1970-01-01 00:00:00 +0000
1830+++ hooks/charmhelpers/core/host.py 2014-07-07 20:28:28 +0000
1831@@ -0,0 +1,325 @@
1832+"""Tools for working with the host system"""
1833+# Copyright 2012 Canonical Ltd.
1834+#
1835+# Authors:
1836+# Nick Moffitt <nick.moffitt@canonical.com>
1837+# Matthew Wedgwood <matthew.wedgwood@canonical.com>
1838+
1839+import os
1840+import pwd
1841+import grp
1842+import random
1843+import string
1844+import subprocess
1845+import hashlib
1846+import apt_pkg
1847+
1848+from collections import OrderedDict
1849+
1850+from hookenv import log
1851+from fstab import Fstab
1852+
1853+
1854+def service_start(service_name):
1855+ """Start a system service"""
1856+ return service('start', service_name)
1857+
1858+
1859+def service_stop(service_name):
1860+ """Stop a system service"""
1861+ return service('stop', service_name)
1862+
1863+
1864+def service_restart(service_name):
1865+ """Restart a system service"""
1866+ return service('restart', service_name)
1867+
1868+
1869+def service_reload(service_name, restart_on_failure=False):
1870+ """Reload a system service, optionally falling back to restart if
1871+ reload fails"""
1872+ service_result = service('reload', service_name)
1873+ if not service_result and restart_on_failure:
1874+ service_result = service('restart', service_name)
1875+ return service_result
1876+
1877+
1878+def service(action, service_name):
1879+ """Control a system service"""
1880+ cmd = ['service', service_name, action]
1881+ return subprocess.call(cmd) == 0
1882+
1883+
1884+def service_running(service):
1885+ """Determine whether a system service is running"""
1886+ try:
1887+ output = subprocess.check_output(['service', service, 'status'])
1888+ except subprocess.CalledProcessError:
1889+ return False
1890+ else:
1891+ if ("start/running" in output or "is running" in output):
1892+ return True
1893+ else:
1894+ return False
1895+
1896+
1897+def adduser(username, password=None, shell='/bin/bash', system_user=False):
1898+ """Add a user to the system"""
1899+ try:
1900+ user_info = pwd.getpwnam(username)
1901+ log('user {0} already exists!'.format(username))
1902+ except KeyError:
1903+ log('creating user {0}'.format(username))
1904+ cmd = ['useradd']
1905+ if system_user or password is None:
1906+ cmd.append('--system')
1907+ else:
1908+ cmd.extend([
1909+ '--create-home',
1910+ '--shell', shell,
1911+ '--password', password,
1912+ ])
1913+ cmd.append(username)
1914+ subprocess.check_call(cmd)
1915+ user_info = pwd.getpwnam(username)
1916+ return user_info
1917+
1918+
1919+def add_user_to_group(username, group):
1920+ """Add a user to a group"""
1921+ cmd = [
1922+ 'gpasswd', '-a',
1923+ username,
1924+ group
1925+ ]
1926+ log("Adding user {} to group {}".format(username, group))
1927+ subprocess.check_call(cmd)
1928+
1929+
1930+def rsync(from_path, to_path, flags='-r', options=None):
1931+ """Replicate the contents of a path"""
1932+ options = options or ['--delete', '--executability']
1933+ cmd = ['/usr/bin/rsync', flags]
1934+ cmd.extend(options)
1935+ cmd.append(from_path)
1936+ cmd.append(to_path)
1937+ log(" ".join(cmd))
1938+ return subprocess.check_output(cmd).strip()
1939+
1940+
1941+def symlink(source, destination):
1942+ """Create a symbolic link"""
1943+ log("Symlinking {} as {}".format(source, destination))
1944+ cmd = [
1945+ 'ln',
1946+ '-sf',
1947+ source,
1948+ destination,
1949+ ]
1950+ subprocess.check_call(cmd)
1951+
1952+
1953+def mkdir(path, owner='root', group='root', perms=0555, force=False):
1954+ """Create a directory"""
1955+ log("Making dir {} {}:{} {:o}".format(path, owner, group,
1956+ perms))
1957+ uid = pwd.getpwnam(owner).pw_uid
1958+ gid = grp.getgrnam(group).gr_gid
1959+ realpath = os.path.abspath(path)
1960+ if os.path.exists(realpath):
1961+ if force and not os.path.isdir(realpath):
1962+ log("Removing non-directory file {} prior to mkdir()".format(path))
1963+ os.unlink(realpath)
1964+ else:
1965+ os.makedirs(realpath, perms)
1966+ os.chown(realpath, uid, gid)
1967+
1968+
1969+def write_file(path, content, owner='root', group='root', perms=0444):
1970+ """Create or overwrite a file with the contents of a string"""
1971+ log("Writing file {} {}:{} {:o}".format(path, owner, group, perms))
1972+ uid = pwd.getpwnam(owner).pw_uid
1973+ gid = grp.getgrnam(group).gr_gid
1974+ with open(path, 'w') as target:
1975+ os.fchown(target.fileno(), uid, gid)
1976+ os.fchmod(target.fileno(), perms)
1977+ target.write(content)
1978+
1979+
1980+def fstab_remove(mp):
1981+ """Remove the given mountpoint entry from /etc/fstab
1982+ """
1983+ return Fstab.remove_by_mountpoint(mp)
1984+
1985+
1986+def fstab_add(dev, mp, fs, options=None):
1987+ """Adds the given device entry to the /etc/fstab file
1988+ """
1989+ return Fstab.add(dev, mp, fs, options=options)
1990+
1991+
1992+def mount(device, mountpoint, options=None, persist=False, filesystem="ext3"):
1993+ """Mount a filesystem at a particular mountpoint"""
1994+ cmd_args = ['mount']
1995+ if options is not None:
1996+ cmd_args.extend(['-o', options])
1997+ cmd_args.extend([device, mountpoint])
1998+ try:
1999+ subprocess.check_output(cmd_args)
2000+ except subprocess.CalledProcessError, e:
2001+ log('Error mounting {} at {}\n{}'.format(device, mountpoint, e.output))
2002+ return False
2003+
2004+ if persist:
2005+ return fstab_add(device, mountpoint, filesystem, options=options)
2006+ return True
2007+
2008+
2009+def umount(mountpoint, persist=False):
2010+ """Unmount a filesystem"""
2011+ cmd_args = ['umount', mountpoint]
2012+ try:
2013+ subprocess.check_output(cmd_args)
2014+ except subprocess.CalledProcessError, e:
2015+ log('Error unmounting {}\n{}'.format(mountpoint, e.output))
2016+ return False
2017+
2018+ if persist:
2019+ return fstab_remove(mountpoint)
2020+ return True
2021+
2022+
2023+def mounts():
2024+ """Get a list of all mounted volumes as [[mountpoint,device],[...]]"""
2025+ with open('/proc/mounts') as f:
2026+ # [['/mount/point','/dev/path'],[...]]
2027+ system_mounts = [m[1::-1] for m in [l.strip().split()
2028+ for l in f.readlines()]]
2029+ return system_mounts
2030+
2031+
2032+def file_hash(path):
2033+ """Generate a md5 hash of the contents of 'path' or None if not found """
2034+ if os.path.exists(path):
2035+ h = hashlib.md5()
2036+ with open(path, 'r') as source:
2037+ h.update(source.read()) # IGNORE:E1101 - it does have update
2038+ return h.hexdigest()
2039+ else:
2040+ return None
2041+
2042+
2043+def restart_on_change(restart_map, stopstart=False):
2044+ """Restart services based on configuration files changing
2045+
2046+ This function is used a decorator, for example
2047+
2048+ @restart_on_change({
2049+ '/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ]
2050+ })
2051+ def ceph_client_changed():
2052+ ...
2053+
2054+ In this example, the cinder-api and cinder-volume services
2055+ would be restarted if /etc/ceph/ceph.conf is changed by the
2056+ ceph_client_changed function.
2057+ """
2058+ def wrap(f):
2059+ def wrapped_f(*args):
2060+ checksums = {}
2061+ for path in restart_map:
2062+ checksums[path] = file_hash(path)
2063+ f(*args)
2064+ restarts = []
2065+ for path in restart_map:
2066+ if checksums[path] != file_hash(path):
2067+ restarts += restart_map[path]
2068+ services_list = list(OrderedDict.fromkeys(restarts))
2069+ if not stopstart:
2070+ for service_name in services_list:
2071+ service('restart', service_name)
2072+ else:
2073+ for action in ['stop', 'start']:
2074+ for service_name in services_list:
2075+ service(action, service_name)
2076+ return wrapped_f
2077+ return wrap
2078+
2079+
2080+def lsb_release():
2081+ """Return /etc/lsb-release in a dict"""
2082+ d = {}
2083+ with open('/etc/lsb-release', 'r') as lsb:
2084+ for l in lsb:
2085+ k, v = l.split('=')
2086+ d[k.strip()] = v.strip()
2087+ return d
2088+
2089+
2090+def pwgen(length=None):
2091+ """Generate a random pasword."""
2092+ if length is None:
2093+ length = random.choice(range(35, 45))
2094+ alphanumeric_chars = [
2095+ l for l in (string.letters + string.digits)
2096+ if l not in 'l0QD1vAEIOUaeiou']
2097+ random_chars = [
2098+ random.choice(alphanumeric_chars) for _ in range(length)]
2099+ return(''.join(random_chars))
2100+
2101+
2102+def list_nics(nic_type):
2103+ '''Return a list of nics of given type(s)'''
2104+ if isinstance(nic_type, basestring):
2105+ int_types = [nic_type]
2106+ else:
2107+ int_types = nic_type
2108+ interfaces = []
2109+ for int_type in int_types:
2110+ cmd = ['ip', 'addr', 'show', 'label', int_type + '*']
2111+ ip_output = subprocess.check_output(cmd).split('\n')
2112+ ip_output = (line for line in ip_output if line)
2113+ for line in ip_output:
2114+ if line.split()[1].startswith(int_type):
2115+ interfaces.append(line.split()[1].replace(":", ""))
2116+ return interfaces
2117+
2118+
2119+def set_nic_mtu(nic, mtu):
2120+ '''Set MTU on a network interface'''
2121+ cmd = ['ip', 'link', 'set', nic, 'mtu', mtu]
2122+ subprocess.check_call(cmd)
2123+
2124+
2125+def get_nic_mtu(nic):
2126+ cmd = ['ip', 'addr', 'show', nic]
2127+ ip_output = subprocess.check_output(cmd).split('\n')
2128+ mtu = ""
2129+ for line in ip_output:
2130+ words = line.split()
2131+ if 'mtu' in words:
2132+ mtu = words[words.index("mtu") + 1]
2133+ return mtu
2134+
2135+
2136+def get_nic_hwaddr(nic):
2137+ cmd = ['ip', '-o', '-0', 'addr', 'show', nic]
2138+ ip_output = subprocess.check_output(cmd)
2139+ hwaddr = ""
2140+ words = ip_output.split()
2141+ if 'link/ether' in words:
2142+ hwaddr = words[words.index('link/ether') + 1]
2143+ return hwaddr
2144+
2145+
2146+def cmp_pkgrevno(package, revno, pkgcache=None):
2147+ '''Compare supplied revno with the revno of the installed package
2148+ 1 => Installed revno is greater than supplied arg
2149+ 0 => Installed revno is the same as supplied arg
2150+ -1 => Installed revno is less than supplied arg
2151+ '''
2152+ if not pkgcache:
2153+ apt_pkg.init()
2154+ pkgcache = apt_pkg.Cache()
2155+ pkg = pkgcache[package]
2156+ return apt_pkg.version_compare(pkg.current_ver.ver_str, revno)
2157
2158=== added directory 'hooks/charmhelpers/fetch'
2159=== added file 'hooks/charmhelpers/fetch/__init__.py'
2160--- hooks/charmhelpers/fetch/__init__.py 1970-01-01 00:00:00 +0000
2161+++ hooks/charmhelpers/fetch/__init__.py 2014-07-07 20:28:28 +0000
2162@@ -0,0 +1,349 @@
2163+import importlib
2164+import time
2165+from yaml import safe_load
2166+from charmhelpers.core.host import (
2167+ lsb_release
2168+)
2169+from urlparse import (
2170+ urlparse,
2171+ urlunparse,
2172+)
2173+import subprocess
2174+from charmhelpers.core.hookenv import (
2175+ config,
2176+ log,
2177+)
2178+import apt_pkg
2179+import os
2180+
2181+
2182+CLOUD_ARCHIVE = """# Ubuntu Cloud Archive
2183+deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main
2184+"""
2185+PROPOSED_POCKET = """# Proposed
2186+deb http://archive.ubuntu.com/ubuntu {}-proposed main universe multiverse restricted
2187+"""
2188+CLOUD_ARCHIVE_POCKETS = {
2189+ # Folsom
2190+ 'folsom': 'precise-updates/folsom',
2191+ 'precise-folsom': 'precise-updates/folsom',
2192+ 'precise-folsom/updates': 'precise-updates/folsom',
2193+ 'precise-updates/folsom': 'precise-updates/folsom',
2194+ 'folsom/proposed': 'precise-proposed/folsom',
2195+ 'precise-folsom/proposed': 'precise-proposed/folsom',
2196+ 'precise-proposed/folsom': 'precise-proposed/folsom',
2197+ # Grizzly
2198+ 'grizzly': 'precise-updates/grizzly',
2199+ 'precise-grizzly': 'precise-updates/grizzly',
2200+ 'precise-grizzly/updates': 'precise-updates/grizzly',
2201+ 'precise-updates/grizzly': 'precise-updates/grizzly',
2202+ 'grizzly/proposed': 'precise-proposed/grizzly',
2203+ 'precise-grizzly/proposed': 'precise-proposed/grizzly',
2204+ 'precise-proposed/grizzly': 'precise-proposed/grizzly',
2205+ # Havana
2206+ 'havana': 'precise-updates/havana',
2207+ 'precise-havana': 'precise-updates/havana',
2208+ 'precise-havana/updates': 'precise-updates/havana',
2209+ 'precise-updates/havana': 'precise-updates/havana',
2210+ 'havana/proposed': 'precise-proposed/havana',
2211+ 'precise-havana/proposed': 'precise-proposed/havana',
2212+ 'precise-proposed/havana': 'precise-proposed/havana',
2213+ # Icehouse
2214+ 'icehouse': 'precise-updates/icehouse',
2215+ 'precise-icehouse': 'precise-updates/icehouse',
2216+ 'precise-icehouse/updates': 'precise-updates/icehouse',
2217+ 'precise-updates/icehouse': 'precise-updates/icehouse',
2218+ 'icehouse/proposed': 'precise-proposed/icehouse',
2219+ 'precise-icehouse/proposed': 'precise-proposed/icehouse',
2220+ 'precise-proposed/icehouse': 'precise-proposed/icehouse',
2221+ # Juno
2222+ 'juno': 'trusty-updates/juno',
2223+ 'trusty-juno': 'trusty-updates/juno',
2224+ 'trusty-juno/updates': 'trusty-updates/juno',
2225+ 'trusty-updates/juno': 'trusty-updates/juno',
2226+ 'juno/proposed': 'trusty-proposed/juno',
2227+ 'juno/proposed': 'trusty-proposed/juno',
2228+ 'trusty-juno/proposed': 'trusty-proposed/juno',
2229+ 'trusty-proposed/juno': 'trusty-proposed/juno',
2230+}
2231+
2232+# The order of this list is very important. Handlers should be listed in from
2233+# least- to most-specific URL matching.
2234+FETCH_HANDLERS = (
2235+ 'charmhelpers.fetch.archiveurl.ArchiveUrlFetchHandler',
2236+ 'charmhelpers.fetch.bzrurl.BzrUrlFetchHandler',
2237+)
2238+
2239+APT_NO_LOCK = 100 # The return code for "couldn't acquire lock" in APT.
2240+APT_NO_LOCK_RETRY_DELAY = 10 # Wait 10 seconds between apt lock checks.
2241+APT_NO_LOCK_RETRY_COUNT = 30 # Retry to acquire the lock X times.
2242+
2243+
2244+class SourceConfigError(Exception):
2245+ pass
2246+
2247+
2248+class UnhandledSource(Exception):
2249+ pass
2250+
2251+
2252+class AptLockError(Exception):
2253+ pass
2254+
2255+
2256+class BaseFetchHandler(object):
2257+
2258+ """Base class for FetchHandler implementations in fetch plugins"""
2259+
2260+ def can_handle(self, source):
2261+ """Returns True if the source can be handled. Otherwise returns
2262+ a string explaining why it cannot"""
2263+ return "Wrong source type"
2264+
2265+ def install(self, source):
2266+ """Try to download and unpack the source. Return the path to the
2267+ unpacked files or raise UnhandledSource."""
2268+ raise UnhandledSource("Wrong source type {}".format(source))
2269+
2270+ def parse_url(self, url):
2271+ return urlparse(url)
2272+
2273+ def base_url(self, url):
2274+ """Return url without querystring or fragment"""
2275+ parts = list(self.parse_url(url))
2276+ parts[4:] = ['' for i in parts[4:]]
2277+ return urlunparse(parts)
2278+
2279+
2280+def filter_installed_packages(packages):
2281+ """Returns a list of packages that require installation"""
2282+ apt_pkg.init()
2283+
2284+ # Tell apt to build an in-memory cache to prevent race conditions (if
2285+ # another process is already building the cache).
2286+ apt_pkg.config.set("Dir::Cache::pkgcache", "")
2287+
2288+ cache = apt_pkg.Cache()
2289+ _pkgs = []
2290+ for package in packages:
2291+ try:
2292+ p = cache[package]
2293+ p.current_ver or _pkgs.append(package)
2294+ except KeyError:
2295+ log('Package {} has no installation candidate.'.format(package),
2296+ level='WARNING')
2297+ _pkgs.append(package)
2298+ return _pkgs
2299+
2300+
2301+def apt_install(packages, options=None, fatal=False):
2302+ """Install one or more packages"""
2303+ if options is None:
2304+ options = ['--option=Dpkg::Options::=--force-confold']
2305+
2306+ cmd = ['apt-get', '--assume-yes']
2307+ cmd.extend(options)
2308+ cmd.append('install')
2309+ if isinstance(packages, basestring):
2310+ cmd.append(packages)
2311+ else:
2312+ cmd.extend(packages)
2313+ log("Installing {} with options: {}".format(packages,
2314+ options))
2315+ _run_apt_command(cmd, fatal)
2316+
2317+
2318+def apt_upgrade(options=None, fatal=False, dist=False):
2319+ """Upgrade all packages"""
2320+ if options is None:
2321+ options = ['--option=Dpkg::Options::=--force-confold']
2322+
2323+ cmd = ['apt-get', '--assume-yes']
2324+ cmd.extend(options)
2325+ if dist:
2326+ cmd.append('dist-upgrade')
2327+ else:
2328+ cmd.append('upgrade')
2329+ log("Upgrading with options: {}".format(options))
2330+ _run_apt_command(cmd, fatal)
2331+
2332+
2333+def apt_update(fatal=False):
2334+ """Update local apt cache"""
2335+ cmd = ['apt-get', 'update']
2336+ _run_apt_command(cmd, fatal)
2337+
2338+
2339+def apt_purge(packages, fatal=False):
2340+ """Purge one or more packages"""
2341+ cmd = ['apt-get', '--assume-yes', 'purge']
2342+ if isinstance(packages, basestring):
2343+ cmd.append(packages)
2344+ else:
2345+ cmd.extend(packages)
2346+ log("Purging {}".format(packages))
2347+ _run_apt_command(cmd, fatal)
2348+
2349+
2350+def apt_hold(packages, fatal=False):
2351+ """Hold one or more packages"""
2352+ cmd = ['apt-mark', 'hold']
2353+ if isinstance(packages, basestring):
2354+ cmd.append(packages)
2355+ else:
2356+ cmd.extend(packages)
2357+ log("Holding {}".format(packages))
2358+
2359+ if fatal:
2360+ subprocess.check_call(cmd)
2361+ else:
2362+ subprocess.call(cmd)
2363+
2364+
2365+def add_source(source, key=None):
2366+ if source is None:
2367+ log('Source is not present. Skipping')
2368+ return
2369+
2370+ if (source.startswith('ppa:') or
2371+ source.startswith('http') or
2372+ source.startswith('deb ') or
2373+ source.startswith('cloud-archive:')):
2374+ subprocess.check_call(['add-apt-repository', '--yes', source])
2375+ elif source.startswith('cloud:'):
2376+ apt_install(filter_installed_packages(['ubuntu-cloud-keyring']),
2377+ fatal=True)
2378+ pocket = source.split(':')[-1]
2379+ if pocket not in CLOUD_ARCHIVE_POCKETS:
2380+ raise SourceConfigError(
2381+ 'Unsupported cloud: source option %s' %
2382+ pocket)
2383+ actual_pocket = CLOUD_ARCHIVE_POCKETS[pocket]
2384+ with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as apt:
2385+ apt.write(CLOUD_ARCHIVE.format(actual_pocket))
2386+ elif source == 'proposed':
2387+ release = lsb_release()['DISTRIB_CODENAME']
2388+ with open('/etc/apt/sources.list.d/proposed.list', 'w') as apt:
2389+ apt.write(PROPOSED_POCKET.format(release))
2390+ if key:
2391+ subprocess.check_call(['apt-key', 'adv', '--keyserver',
2392+ 'hkp://keyserver.ubuntu.com:80', '--recv',
2393+ key])
2394+
2395+
2396+def configure_sources(update=False,
2397+ sources_var='install_sources',
2398+ keys_var='install_keys'):
2399+ """
2400+ Configure multiple sources from charm configuration
2401+
2402+ Example config:
2403+ install_sources:
2404+ - "ppa:foo"
2405+ - "http://example.com/repo precise main"
2406+ install_keys:
2407+ - null
2408+ - "a1b2c3d4"
2409+
2410+ Note that 'null' (a.k.a. None) should not be quoted.
2411+ """
2412+ sources = safe_load(config(sources_var))
2413+ keys = config(keys_var)
2414+ if keys is not None:
2415+ keys = safe_load(keys)
2416+ if isinstance(sources, basestring) and (
2417+ keys is None or isinstance(keys, basestring)):
2418+ add_source(sources, keys)
2419+ else:
2420+ if not len(sources) == len(keys):
2421+ msg = 'Install sources and keys lists are different lengths'
2422+ raise SourceConfigError(msg)
2423+ for src_num in range(len(sources)):
2424+ add_source(sources[src_num], keys[src_num])
2425+ if update:
2426+ apt_update(fatal=True)
2427+
2428+
2429+def install_remote(source):
2430+ """
2431+ Install a file tree from a remote source
2432+
2433+ The specified source should be a url of the form:
2434+ scheme://[host]/path[#[option=value][&...]]
2435+
2436+ Schemes supported are based on this modules submodules
2437+ Options supported are submodule-specific"""
2438+ # We ONLY check for True here because can_handle may return a string
2439+ # explaining why it can't handle a given source.
2440+ handlers = [h for h in plugins() if h.can_handle(source) is True]
2441+ installed_to = None
2442+ for handler in handlers:
2443+ try:
2444+ installed_to = handler.install(source)
2445+ except UnhandledSource:
2446+ pass
2447+ if not installed_to:
2448+ raise UnhandledSource("No handler found for source {}".format(source))
2449+ return installed_to
2450+
2451+
2452+def install_from_config(config_var_name):
2453+ charm_config = config()
2454+ source = charm_config[config_var_name]
2455+ return install_remote(source)
2456+
2457+
2458+def plugins(fetch_handlers=None):
2459+ if not fetch_handlers:
2460+ fetch_handlers = FETCH_HANDLERS
2461+ plugin_list = []
2462+ for handler_name in fetch_handlers:
2463+ package, classname = handler_name.rsplit('.', 1)
2464+ try:
2465+ handler_class = getattr(
2466+ importlib.import_module(package),
2467+ classname)
2468+ plugin_list.append(handler_class())
2469+ except (ImportError, AttributeError):
2470+ # Skip missing plugins so that they can be ommitted from
2471+ # installation if desired
2472+ log("FetchHandler {} not found, skipping plugin".format(
2473+ handler_name))
2474+ return plugin_list
2475+
2476+
2477+def _run_apt_command(cmd, fatal=False):
2478+ """
2479+ Run an APT command, checking output and retrying if the fatal flag is set
2480+ to True.
2481+
2482+ :param: cmd: str: The apt command to run.
2483+ :param: fatal: bool: Whether the command's output should be checked and
2484+ retried.
2485+ """
2486+ env = os.environ.copy()
2487+
2488+ if 'DEBIAN_FRONTEND' not in env:
2489+ env['DEBIAN_FRONTEND'] = 'noninteractive'
2490+
2491+ if fatal:
2492+ retry_count = 0
2493+ result = None
2494+
2495+ # If the command is considered "fatal", we need to retry if the apt
2496+ # lock was not acquired.
2497+
2498+ while result is None or result == APT_NO_LOCK:
2499+ try:
2500+ result = subprocess.check_call(cmd, env=env)
2501+ except subprocess.CalledProcessError, e:
2502+ retry_count = retry_count + 1
2503+ if retry_count > APT_NO_LOCK_RETRY_COUNT:
2504+ raise
2505+ result = e.returncode
2506+ log("Couldn't acquire DPKG lock. Will retry in {} seconds."
2507+ "".format(APT_NO_LOCK_RETRY_DELAY))
2508+ time.sleep(APT_NO_LOCK_RETRY_DELAY)
2509+
2510+ else:
2511+ subprocess.call(cmd, env=env)
2512
2513=== added file 'hooks/charmhelpers/fetch/archiveurl.py'
2514--- hooks/charmhelpers/fetch/archiveurl.py 1970-01-01 00:00:00 +0000
2515+++ hooks/charmhelpers/fetch/archiveurl.py 2014-07-07 20:28:28 +0000
2516@@ -0,0 +1,63 @@
2517+import os
2518+import urllib2
2519+import urlparse
2520+
2521+from charmhelpers.fetch import (
2522+ BaseFetchHandler,
2523+ UnhandledSource
2524+)
2525+from charmhelpers.payload.archive import (
2526+ get_archive_handler,
2527+ extract,
2528+)
2529+from charmhelpers.core.host import mkdir
2530+
2531+
2532+class ArchiveUrlFetchHandler(BaseFetchHandler):
2533+ """Handler for archives via generic URLs"""
2534+ def can_handle(self, source):
2535+ url_parts = self.parse_url(source)
2536+ if url_parts.scheme not in ('http', 'https', 'ftp', 'file'):
2537+ return "Wrong source type"
2538+ if get_archive_handler(self.base_url(source)):
2539+ return True
2540+ return False
2541+
2542+ def download(self, source, dest):
2543+ # propogate all exceptions
2544+ # URLError, OSError, etc
2545+ proto, netloc, path, params, query, fragment = urlparse.urlparse(source)
2546+ if proto in ('http', 'https'):
2547+ auth, barehost = urllib2.splituser(netloc)
2548+ if auth is not None:
2549+ source = urlparse.urlunparse((proto, barehost, path, params, query, fragment))
2550+ username, password = urllib2.splitpasswd(auth)
2551+ passman = urllib2.HTTPPasswordMgrWithDefaultRealm()
2552+ # Realm is set to None in add_password to force the username and password
2553+ # to be used whatever the realm
2554+ passman.add_password(None, source, username, password)
2555+ authhandler = urllib2.HTTPBasicAuthHandler(passman)
2556+ opener = urllib2.build_opener(authhandler)
2557+ urllib2.install_opener(opener)
2558+ response = urllib2.urlopen(source)
2559+ try:
2560+ with open(dest, 'w') as dest_file:
2561+ dest_file.write(response.read())
2562+ except Exception as e:
2563+ if os.path.isfile(dest):
2564+ os.unlink(dest)
2565+ raise e
2566+
2567+ def install(self, source):
2568+ url_parts = self.parse_url(source)
2569+ dest_dir = os.path.join(os.environ.get('CHARM_DIR'), 'fetched')
2570+ if not os.path.exists(dest_dir):
2571+ mkdir(dest_dir, perms=0755)
2572+ dld_file = os.path.join(dest_dir, os.path.basename(url_parts.path))
2573+ try:
2574+ self.download(source, dld_file)
2575+ except urllib2.URLError as e:
2576+ raise UnhandledSource(e.reason)
2577+ except OSError as e:
2578+ raise UnhandledSource(e.strerror)
2579+ return extract(dld_file)
2580
2581=== added file 'hooks/charmhelpers/fetch/bzrurl.py'
2582--- hooks/charmhelpers/fetch/bzrurl.py 1970-01-01 00:00:00 +0000
2583+++ hooks/charmhelpers/fetch/bzrurl.py 2014-07-07 20:28:28 +0000
2584@@ -0,0 +1,50 @@
2585+import os
2586+from charmhelpers.fetch import (
2587+ BaseFetchHandler,
2588+ UnhandledSource
2589+)
2590+from charmhelpers.core.host import mkdir
2591+
2592+try:
2593+ from bzrlib.branch import Branch
2594+except ImportError:
2595+ from charmhelpers.fetch import apt_install
2596+ apt_install("python-bzrlib")
2597+ from bzrlib.branch import Branch
2598+
2599+
2600+class BzrUrlFetchHandler(BaseFetchHandler):
2601+ """Handler for bazaar branches via generic and lp URLs"""
2602+ def can_handle(self, source):
2603+ url_parts = self.parse_url(source)
2604+ if url_parts.scheme not in ('bzr+ssh', 'lp'):
2605+ return False
2606+ else:
2607+ return True
2608+
2609+ def branch(self, source, dest):
2610+ url_parts = self.parse_url(source)
2611+ # If we use lp:branchname scheme we need to load plugins
2612+ if not self.can_handle(source):
2613+ raise UnhandledSource("Cannot handle {}".format(source))
2614+ if url_parts.scheme == "lp":
2615+ from bzrlib.plugin import load_plugins
2616+ load_plugins()
2617+ try:
2618+ remote_branch = Branch.open(source)
2619+ remote_branch.bzrdir.sprout(dest).open_branch()
2620+ except Exception as e:
2621+ raise e
2622+
2623+ def install(self, source):
2624+ url_parts = self.parse_url(source)
2625+ branch_name = url_parts.path.strip("/").split("/")[-1]
2626+ dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",
2627+ branch_name)
2628+ if not os.path.exists(dest_dir):
2629+ mkdir(dest_dir, perms=0755)
2630+ try:
2631+ self.branch(source, dest_dir)
2632+ except OSError as e:
2633+ raise UnhandledSource(e.strerror)
2634+ return dest_dir
2635
2636=== modified file 'hooks/hooks.py'
2637--- hooks/hooks.py 2013-06-18 18:35:01 +0000
2638+++ hooks/hooks.py 2014-07-07 20:28:28 +0000
2639@@ -1,854 +1,38 @@
2640 #!/usr/bin/env python
2641-# vim: et ai ts=4 sw=4:
2642
2643-import json
2644-import os
2645-import re
2646-import subprocess
2647 import sys
2648-import time
2649-from pwd import getpwnam
2650-from grp import getgrnam
2651-from random import choice
2652-
2653-CHARM_PACKAGES = ["python-pip", "python-jinja2", "mercurial", "git-core",
2654- "subversion", "bzr", "gettext"]
2655-
2656-INJECTED_WARNING = """
2657-#------------------------------------------------------------------------------
2658-# The following is the import code for the settings directory injected by Juju
2659-#------------------------------------------------------------------------------
2660-"""
2661-
2662-
2663-###############################################################################
2664-# Supporting functions
2665-###############################################################################
2666-MSG_CRITICAL = "CRITICAL"
2667-MSG_DEBUG = "DEBUG"
2668-MSG_INFO = "INFO"
2669-MSG_ERROR = "ERROR"
2670-MSG_WARNING = "WARNING"
2671-
2672-
2673-def juju_log(level, msg):
2674- subprocess.call(['juju-log', '-l', level, msg])
2675-
2676-#------------------------------------------------------------------------------
2677-# run: Run a command, return the output
2678-#------------------------------------------------------------------------------
2679-def run(command, exit_on_error=True, cwd=None):
2680- try:
2681- juju_log(MSG_DEBUG, command)
2682- return subprocess.check_output(
2683- command, stderr=subprocess.STDOUT, shell=True, cwd=cwd)
2684- except subprocess.CalledProcessError, e:
2685- juju_log(MSG_ERROR, "status=%d, output=%s" % (e.returncode, e.output))
2686- if exit_on_error:
2687- sys.exit(e.returncode)
2688- else:
2689- raise
2690-
2691-
2692-#------------------------------------------------------------------------------
2693-# install_file: install a file resource. overwites existing files.
2694-#------------------------------------------------------------------------------
2695-def install_file(contents, dest, owner="root", group="root", mode=0600):
2696- uid = getpwnam(owner)[2]
2697- gid = getgrnam(group)[2]
2698- dest_fd = os.open(dest, os.O_WRONLY | os.O_TRUNC | os.O_CREAT, mode)
2699- os.fchown(dest_fd, uid, gid)
2700- with os.fdopen(dest_fd, 'w') as destfile:
2701- destfile.write(str(contents))
2702-
2703-
2704-#------------------------------------------------------------------------------
2705-# install_dir: create a directory
2706-#------------------------------------------------------------------------------
2707-def install_dir(dirname, owner="root", group="root", mode=0700):
2708- command = \
2709- '/usr/bin/install -o {} -g {} -m {} -d {}'.format(owner, group, oct(mode),
2710- dirname)
2711- return run(command)
2712-
2713-#------------------------------------------------------------------------------
2714-# config_get: Returns a dictionary containing all of the config information
2715-# Optional parameter: scope
2716-# scope: limits the scope of the returned configuration to the
2717-# desired config item.
2718-#------------------------------------------------------------------------------
2719-def config_get(scope=None):
2720- try:
2721- config_cmd_line = ['config-get']
2722- if scope is not None:
2723- config_cmd_line.append(scope)
2724- config_cmd_line.append('--format=json')
2725- config_data = json.loads(subprocess.check_output(config_cmd_line))
2726- except:
2727- config_data = None
2728- finally:
2729- return(config_data)
2730-
2731-
2732-#------------------------------------------------------------------------------
2733-# relation_json: Returns json-formatted relation data
2734-# Optional parameters: scope, relation_id
2735-# scope: limits the scope of the returned data to the
2736-# desired item.
2737-# unit_name: limits the data ( and optionally the scope )
2738-# to the specified unit
2739-# relation_id: specify relation id for out of context usage.
2740-#------------------------------------------------------------------------------
2741-def relation_json(scope=None, unit_name=None, relation_id=None):
2742- command = ['relation-get', '--format=json']
2743- if relation_id is not None:
2744- command.extend(('-r', relation_id))
2745- if scope is not None:
2746- command.append(scope)
2747- else:
2748- command.append('-')
2749- if unit_name is not None:
2750- command.append(unit_name)
2751- output = subprocess.check_output(command, stderr=subprocess.STDOUT)
2752- return output or None
2753-
2754-
2755-#------------------------------------------------------------------------------
2756-# relation_get: Returns a dictionary containing the relation information
2757-# Optional parameters: scope, relation_id
2758-# scope: limits the scope of the returned data to the
2759-# desired item.
2760-# unit_name: limits the data ( and optionally the scope )
2761-# to the specified unit
2762-#------------------------------------------------------------------------------
2763-def relation_get(scope=None, unit_name=None, relation_id=None):
2764- j = relation_json(scope, unit_name, relation_id)
2765- if j:
2766- return json.loads(j)
2767- else:
2768- return None
2769-
2770-
2771-def relation_set(keyvalues, relation_id=None):
2772- args = []
2773- if relation_id:
2774- args.extend(['-r', relation_id])
2775- args.extend(["{}='{}'".format(k, v or '') for k, v in keyvalues.items()])
2776- run("relation-set {}".format(' '.join(args)))
2777-
2778- ## Posting json to relation-set doesn't seem to work as documented?
2779- ## Bug #1116179
2780- ##
2781- ## cmd = ['relation-set']
2782- ## if relation_id:
2783- ## cmd.extend(['-r', relation_id])
2784- ## p = Popen(
2785- ## cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
2786- ## stderr=subprocess.PIPE)
2787- ## (out, err) = p.communicate(json.dumps(keyvalues))
2788- ## if p.returncode:
2789- ## juju_log(MSG_ERROR, err)
2790- ## sys.exit(1)
2791- ## juju_log(MSG_DEBUG, "relation-set {}".format(repr(keyvalues)))
2792-
2793-
2794-def relation_list(relation_id=None):
2795- """Return the list of units participating in the relation."""
2796- if relation_id is None:
2797- relation_id = os.environ['JUJU_RELATION_ID']
2798- cmd = ['relation-list', '--format=json', '-r', relation_id]
2799- json_units = subprocess.check_output(cmd).strip()
2800- if json_units:
2801- return json.loads(subprocess.check_output(cmd))
2802- return []
2803-
2804-
2805-#------------------------------------------------------------------------------
2806-# relation_ids: Returns a list of relation ids
2807-# optional parameters: relation_type
2808-# relation_type: return relations only of this type
2809-#------------------------------------------------------------------------------
2810-def relation_ids(relation_types=('db',)):
2811- # accept strings or iterators
2812- if isinstance(relation_types, basestring):
2813- reltypes = [relation_types, ]
2814- else:
2815- reltypes = relation_types
2816- relids = []
2817- for reltype in reltypes:
2818- relid_cmd_line = ['relation-ids', '--format=json', reltype]
2819- json_relids = subprocess.check_output(relid_cmd_line).strip()
2820- if json_relids:
2821- relids.extend(json.loads(json_relids))
2822- return relids
2823-
2824-
2825-#------------------------------------------------------------------------------
2826-# relation_get_all: Returns a dictionary containing the relation information
2827-# optional parameters: relation_type
2828-# relation_type: limits the scope of the returned data to the
2829-# desired item.
2830-#------------------------------------------------------------------------------
2831-def relation_get_all(*args, **kwargs):
2832- relation_data = []
2833- relids = relation_ids(*args, **kwargs)
2834- for relid in relids:
2835- units_cmd_line = ['relation-list', '--format=json', '-r', relid]
2836- json_units = subprocess.check_output(units_cmd_line).strip()
2837- if json_units:
2838- for unit in json.loads(json_units):
2839- unit_data = \
2840- json.loads(relation_json(relation_id=relid,
2841- unit_name=unit))
2842- for key in unit_data:
2843- if key.endswith('-list'):
2844- unit_data[key] = unit_data[key].split()
2845- unit_data['relation-id'] = relid
2846- unit_data['unit'] = unit
2847- relation_data.append(unit_data)
2848- return relation_data
2849-
2850-def apt_get_update():
2851- cmd_line = ['apt-get', 'update']
2852- return(subprocess.call(cmd_line))
2853-
2854-
2855-#------------------------------------------------------------------------------
2856-# apt_get_install( packages ): Installs package(s)
2857-#------------------------------------------------------------------------------
2858-def apt_get_install(packages=None):
2859- if packages is None:
2860- return(False)
2861- cmd_line = ['apt-get', '-y', 'install', '-qq']
2862- if isinstance(packages, list):
2863- cmd_line.extend(packages)
2864- else:
2865- cmd_line.append(packages)
2866- return(subprocess.call(cmd_line))
2867-
2868-
2869-#------------------------------------------------------------------------------
2870-# pip_install( package ): Installs a python package
2871-#------------------------------------------------------------------------------
2872-def pip_install(packages=None, upgrade=False):
2873- # Build in /tmp or Juju's internal git will be confused
2874- cmd_line = ['pip', 'install', '-b', '/tmp/']
2875- if packages is None:
2876- return(False)
2877- if upgrade:
2878- cmd_line.append('--upgrade')
2879- if not isinstance(packages, list):
2880- packages = [packages]
2881-
2882- for package in packages:
2883- if package.startswith('svn+') or package.startswith('git+') or \
2884- package.startswith('hg+') or package.startswith('bzr+'):
2885- cmd_line.append('-e')
2886- cmd_line.append(package)
2887-
2888- cmd_line.append('--use-mirrors')
2889- return(subprocess.call(cmd_line))
2890-
2891-#------------------------------------------------------------------------------
2892-# pip_install_req( path ): Installs a requirements file
2893-#------------------------------------------------------------------------------
2894-def pip_install_req(path=None, upgrade=False):
2895- # Build in /tmp or Juju's internal git will be confused
2896- cmd_line = ['pip', 'install', '-b', '/tmp/']
2897- if path is None:
2898- return(False)
2899- if upgrade:
2900- cmd_line.append('--upgrade')
2901- cmd_line.append('-r')
2902- cmd_line.append(path)
2903- cwd = os.path.dirname(path)
2904- cmd_line.append('--use-mirrors')
2905- return(subprocess.call(cmd_line, cwd=cwd))
2906-
2907-#------------------------------------------------------------------------------
2908-# open_port: Convenience function to open a port in juju to
2909-# expose a service
2910-#------------------------------------------------------------------------------
2911-def open_port(port=None, protocol="TCP"):
2912- if port is None:
2913- return(None)
2914- return(subprocess.call(['open-port', "%d/%s" %
2915- (int(port), protocol)]))
2916-
2917-
2918-#------------------------------------------------------------------------------
2919-# close_port: Convenience function to close a port in juju to
2920-# unexpose a service
2921-#------------------------------------------------------------------------------
2922-def close_port(port=None, protocol="TCP"):
2923- if port is None:
2924- return(None)
2925- return(subprocess.call(['close-port', "%d/%s" %
2926- (int(port), protocol)]))
2927-
2928-
2929-#------------------------------------------------------------------------------
2930-# update_service_ports: Convenience function that evaluate the old and new
2931-# service ports to decide which ports need to be
2932-# opened and which to close
2933-#------------------------------------------------------------------------------
2934-def update_service_port(old_service_port=None, new_service_port=None):
2935- if old_service_port is None or new_service_port is None:
2936- return(None)
2937- if new_service_port != old_service_port:
2938- close_port(old_service_port)
2939- open_port(new_service_port)
2940-
2941-#
2942-# Utils
2943-#
2944-
2945-def install_or_append(contents, dest, owner="root", group="root", mode=0600):
2946- if os.path.exists(dest):
2947- uid = getpwnam(owner)[2]
2948- gid = getgrnam(group)[2]
2949- dest_fd = os.open(dest, os.O_APPEND, mode)
2950- os.fchown(dest_fd, uid, gid)
2951- with os.fdopen(dest_fd, 'a') as destfile:
2952- destfile.write(str(contents))
2953- else:
2954- install_file(contents, dest, owner, group, mode)
2955-
2956-def token_sql_safe(value):
2957- # Only allow alphanumeric + underscore in database identifiers
2958- if re.search('[^A-Za-z0-9_]', value):
2959- return False
2960- return True
2961-
2962-def sanitize(s):
2963- s = s.replace(':', '_')
2964- s = s.replace('-', '_')
2965- s = s.replace('/', '_')
2966- s = s.replace('"', '_')
2967- s = s.replace("'", '_')
2968- return s
2969-
2970-def user_name(relid, remote_unit, admin=False, schema=False):
2971- components = [sanitize(relid), sanitize(remote_unit)]
2972- if admin:
2973- components.append("admin")
2974- elif schema:
2975- components.append("schema")
2976- return "_".join(components)
2977-
2978-def get_relation_host():
2979- remote_host = run("relation-get ip")
2980- if not remote_host:
2981- # remote unit $JUJU_REMOTE_UNIT uses deprecated 'ip=' component of
2982- # interface.
2983- remote_host = run("relation-get private-address")
2984- return remote_host
2985-
2986-
2987-def get_unit_host():
2988- this_host = run("unit-get private-address")
2989- return this_host.strip()
2990-
2991-def process_template(template_name, template_vars, destination):
2992- # --- exported service configuration file
2993- from jinja2 import Environment, FileSystemLoader
2994- template_env = Environment(
2995- loader=FileSystemLoader(os.path.join(os.environ['CHARM_DIR'],
2996- 'templates')))
2997-
2998- template = \
2999- template_env.get_template(template_name).render(template_vars)
3000-
3001- with open(destination, 'w') as inject_file:
3002- inject_file.write(str(template))
3003-
3004-def configure_and_install(rel):
3005-
3006- def _import_key(id):
3007- cmd = "apt-key adv --keyserver keyserver.ubuntu.com " \
3008- "--recv-keys %s" % id
3009- try:
3010- subprocess.check_call(cmd.split(' '))
3011- except:
3012- juju_log(MSG_ERROR, "Error importing repo key %s" % id)
3013-
3014- if rel == 'distro':
3015- return apt_get_install("python-django")
3016- elif rel[:4] == "ppa:":
3017- src = rel
3018- subprocess.check_call(["add-apt-repository", "-y", src])
3019-
3020- return apt_get_install("python-django")
3021- elif rel[:3] == "deb":
3022- l = len(rel.split('|'))
3023- if l == 2:
3024- src, key = rel.split('|')
3025- juju_log("Importing PPA key from keyserver for %s" % src)
3026- _import_key(key)
3027- elif l == 1:
3028- src = rel
3029- else:
3030- juju_log(MSG_ERROR, "Invalid django-release: %s" % rel)
3031-
3032- with open('/etc/apt/sources.list.d/juju_python_django_deb.list', 'w') as f:
3033- f.write(src)
3034-
3035- return apt_get_install("python-django")
3036- elif rel == '':
3037- return pip_install('Django')
3038- else:
3039- return pip_install(rel)
3040-
3041-#
3042-# from:
3043-# http://stackoverflow.com/questions/377017/test-if-executable-exists-in-python
3044-#
3045-def which(program):
3046- def is_exe(fpath):
3047- return os.path.isfile(fpath) and os.access(fpath, os.X_OK)
3048-
3049- for path in os.environ["PATH"].split(os.pathsep):
3050- path = path.strip('"')
3051- exe_file = os.path.join(path, program)
3052- if is_exe(exe_file):
3053- return exe_file
3054-
3055- return False
3056-
3057-def find_django_admin_cmd():
3058- for cmd in ['django-admin.py', 'django-admin']:
3059- django_admin_cmd = which(cmd)
3060- if django_admin_cmd:
3061- return django_admin_cmd
3062-
3063- juju_log(MSG_ERROR, "No django-admin executable found.")
3064-
3065-def append_template(template_name, template_vars, path, try_append=False):
3066-
3067- # --- exported service configuration file
3068- from jinja2 import Environment, FileSystemLoader
3069- template_env = Environment(
3070- loader=FileSystemLoader(os.path.join(os.environ['CHARM_DIR'],
3071- 'templates')))
3072-
3073- template = \
3074- template_env.get_template(template_name).render(template_vars)
3075-
3076- append = False
3077- if os.path.exists(path):
3078- with open(path, 'r') as inject_file:
3079- if not str(template) in inject_file:
3080- append = True
3081- else:
3082- append = True
3083-
3084- if append == True:
3085- with open(path, 'a') as inject_file:
3086- inject_file.write(INJECTED_WARNING)
3087- inject_file.write(str(template))
3088-
3089-
3090-
3091-###############################################################################
3092-# Hook functions
3093-###############################################################################
3094+import charmhelpers.contrib.ansible
3095+
3096+# Create the hooks helper, passing a list of hooks which will be
3097+# handled by default by running all sections of the playbook
3098+# tagged with the hook name.
3099+hooks = charmhelpers.contrib.ansible.AnsibleHooks(
3100+ playbook_path='playbooks/site.yml',
3101+ default_hooks=[
3102+ 'config-changed', 'upgrade-charm',
3103+ 'amqp-relation-broken', 'amqp-relation-changed', 'amqp-relation-joined',
3104+ 'cache-relation-broken', 'cache-relation-changed', 'cache-relation-joined',
3105+ 'django-settings-relation-broken', 'django-settings-relation-changed', 'django-settings-relation-joined',
3106+ 'mongodb-relation-broken', 'mongodb-relation-changed', 'mongodb-relation-joined',
3107+ 'mysql-relation-broken', 'mysql-relation-changed', 'mysql-relation-joined',
3108+ 'mysql-root-relation-broken', 'mysql-root-relation-changed', 'mysql-root-relation-joined',
3109+ 'mysql-shared-relation-broken', 'mysql-shared-relation-changed', 'mysql-shared-relation-joined',
3110+ 'pgsql-relation-broken', 'pgsql-relation-changed', 'pgsql-relation-joined',
3111+ 'redis-relation-broken', 'redis-relation-changed', 'redis-relation-joined',
3112+ 'wsgi-relation-broken', 'wsgi-relation-changed', 'wsgi-relation-joined'
3113+ ])
3114+
3115+
3116+@hooks.hook()
3117 def install():
3118-
3119- for retry in xrange(0,24):
3120- if apt_get_install(CHARM_PACKAGES):
3121- time.sleep(10)
3122- else:
3123- break
3124-
3125- configure_and_install(django_version)
3126-
3127- django_admin_cmd = find_django_admin_cmd()
3128-
3129- if extra_deb_pkgs:
3130- apt_get_install(extra_deb_pkgs.split(','))
3131-
3132- if extra_pip_pkgs:
3133- for package in extra_pip_pkgs.split(','):
3134- pip_install(package)
3135-
3136- if repos_username:
3137- m = re.match(".*://([^/]+)/.*", repos_url)
3138- if m is not None:
3139- repos_domain = m.group(1)
3140- template_vars = {
3141- 'repos_domain': repos_domain,
3142- 'repos_username': repos_username,
3143- 'repos_password': repos_password
3144- }
3145- from os.path import expanduser
3146- process_template('netrc.tmpl', template_vars, expanduser('~/.netrc'))
3147- else:
3148- juju_log(MSG_ERROR, '''Failed to process repos_username and repos_password:\n
3149- cannot identify domain in URL {0}'''.format(repos_url))
3150-
3151- if vcs == 'hg' or vcs == 'mercurial':
3152- run('hg clone %s %s' % (repos_url, vcs_clone_dir))
3153- elif vcs == 'git' or vcs == 'git-core':
3154- if repos_branch:
3155- run('git clone %s -b %s %s' % (repos_url, repos_branch, vcs_clone_dir))
3156- else:
3157- run('git clone %s %s' % (repos_url, vcs_clone_dir))
3158- elif vcs == 'bzr' or vcs == 'bazaar':
3159- run('bzr branch %s %s' % (repos_url, vcs_clone_dir))
3160- elif vcs == 'svn' or vcs == 'subversion':
3161- run('svn co %s %s' % (repos_url, vcs_clone_dir))
3162- elif vcs == '' and repos_url == '':
3163- juju_log(MSG_INFO, "No version control using django-admin startproject")
3164- cmd = '%s startproject' % django_admin_cmd
3165- if project_template_url:
3166- cmd = " ".join([cmd, '--template', project_template_url])
3167- if project_template_extension:
3168- cmd = " ".join([cmd, '--extension', project_template_extension])
3169- try:
3170- run('%s %s %s' % (cmd, sanitized_unit_name, install_root), exit_on_error=False)
3171- except subprocess.CalledProcessError:
3172- run('%s %s' % (cmd, sanitized_unit_name), cwd=install_root)
3173-
3174- else:
3175- juju_log(MSG_ERROR, "Unknown version control")
3176- sys.exit(1)
3177-
3178- run('chown -R %s:%s %s' % (wsgi_user,wsgi_group, working_dir))
3179-
3180- install_dir(settings_dir_path, owner=wsgi_user, group=wsgi_group, mode=0755)
3181- install_dir(urls_dir_path, owner=wsgi_user, group=wsgi_group, mode=0755)
3182-
3183- #FIXME: Upgrades/pulls will mess those files
3184-
3185- for path, dir in ((settings_py_path, 'juju_settings'), (urls_py_path, 'juju_urls')):
3186- append_template('conf_injection.tmpl', {'dir': dir}, path)
3187-
3188- if requirements_pip_files:
3189- for req_file in requirements_pip_files.split(','):
3190- pip_install_req(os.path.join(working_dir,req_file))
3191-
3192- wsgi_py_path = os.path.join(working_dir, 'wsgi.py')
3193- if not os.path.exists(wsgi_py_path):
3194- process_template('wsgi.py.tmpl', {'project_name': sanitized_unit_name, \
3195- 'django_settings': django_settings}, \
3196- wsgi_py_path)
3197-
3198-def start():
3199- if os.path.exists(os.path.join('/etc/init/', sanitized_unit_name + '.conf')):
3200- run("service %s restart || service %s start" % (sanitized_unit_name, sanitized_unit_name))
3201-
3202-def stop():
3203- if os.path.exists(os.path.join('/etc/init/', sanitized_unit_name + '.conf')):
3204- run('service %s stop' % sanitized_unit_name)
3205-
3206-def config_changed(config_data):
3207- os.environ['DJANGO_SETTINGS_MODULE'] = django_settings_modules
3208- django_admin_cmd = find_django_admin_cmd()
3209-
3210- site_secret_key = config_data['site_secret_key']
3211- if not site_secret_key:
3212- site_secret_key = ''.join([choice('abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)') for i in range(50)])
3213-
3214- process_template('secret.tmpl', {'site_secret_key': site_secret_key}, settings_secret_path)
3215-
3216- # Trigger WSGI reloading
3217- for relid in relation_ids('wsgi'):
3218- relation_set({'wsgi_timestamp': time.time()}, relation_id=relid)
3219-
3220-def upgrade():
3221- if extra_pip_pkgs:
3222- for package in extra_pip_pkgs.split(','):
3223- pip_install(package, upgrade=True)
3224-
3225- apt_get_update()
3226- for retry in xrange(0,24):
3227- if apt_get_install(CHARM_PACKAGES):
3228- time.sleep(10)
3229- else:
3230- break
3231-
3232- if vcs == 'hg' or vcs == 'mercurial':
3233- run('hg pull %s %s' % (repos_url, vcs_clone_dir))
3234- elif vcs == 'git' or vcs == 'git-core':
3235- if repos_branch:
3236- run('git pull %s -b %s %s' % (repos_url, repos_branch, vcs_clone_dir))
3237- else:
3238- run('git pull %s %s' % (repos_url, vcs_clone_dir))
3239- elif vcs == 'bzr' or vcs == 'bazaar':
3240- run('bzr pull %s %s' % (repos_url, vcs_clone_dir))
3241- elif vcs == 'svn' or vcs == 'subversion':
3242- run('svn up %s %s' % (repos_url, vcs_clone_dir))
3243- else:
3244- juju_log(MSG_ERROR, "Unknown version control")
3245- sys.exit(1)
3246-
3247- run('chown -R %s:%s %s' % (wsgi_user,wsgi_group, working_dir))
3248-
3249- if requirements_pip_files:
3250- for req_file in requirements_pip_files.split(','):
3251- pip_install_req(os.path.join(working_dir,req_file), upgrade=True)
3252-
3253-
3254- # Trigger WSGI reloading
3255- for relid in relation_ids('wsgi'):
3256- relation_set({'wsgi_timestamp': time.time()}, relation_id=relid)
3257-
3258- for relid in relation_ids('django-settings'):
3259- relation_set({'django_settings_timestamp': time.time()}, relation_id=relid)
3260-
3261-
3262-def django_settings_relation_joined_changed():
3263- os.environ['DJANGO_SETTINGS_MODULE'] = '.'.join([sanitized_unit_name, 'settings'])
3264- django_admin_cmd = find_django_admin_cmd()
3265-
3266- relation_set({'settings_dir_path': settings_dir_path,
3267- 'urls_dir_path': urls_dir_path,
3268- 'install_root': install_root,
3269- 'django_admin_cmd': django_admin_cmd,
3270- 'wsgi_user': wsgi_user,
3271- 'wsgi_group': wsgi_group,
3272- })
3273-
3274- run('chown -R %s:%s %s' % (wsgi_user,wsgi_group, working_dir))
3275-
3276- # Trigger WSGI reloading
3277- for relid in relation_ids('wsgi'):
3278- relation_set({'wsgi_timestamp': time.time()}, relation_id=relid)
3279-
3280-def django_settings_relation_broken():
3281- pass
3282-
3283-def pgsql_relation_joined_changed():
3284- os.environ['DJANGO_SETTINGS_MODULE'] = '.'.join([sanitized_unit_name, 'settings'])
3285- django_admin_cmd = find_django_admin_cmd()
3286-
3287- packages = ["python-psycopg2", "postgresql-client"]
3288- apt_get_install(packages)
3289-
3290- database = relation_get("database")
3291- if not database:
3292- return
3293-
3294- templ_vars = {
3295- 'db_engine': 'django.db.backends.postgresql_psycopg2',
3296- 'db_database': database,
3297- 'db_user': relation_get("user"),
3298- 'db_password': relation_get("password"),
3299- 'db_host': relation_get("host"),
3300- }
3301-
3302- process_template('engine.tmpl', templ_vars, settings_database_path % {'engine_name': 'pgsql'})
3303-
3304- run("%s syncdb --noinput --pythonpath=%s --settings=%s || true" % \
3305- (django_admin_cmd, install_root, django_settings_modules))
3306-
3307-
3308- run('chown -R %s:%s %s' % (wsgi_user,wsgi_group, working_dir))
3309-
3310- # Trigger WSGI reloading
3311- for relid in relation_ids('wsgi'):
3312- relation_set({'wsgi_timestamp': time.time()}, relation_id=relid)
3313-
3314-def pgsql_relation_broken():
3315- run('rm %s' % settings_database_path % {'engine_name': 'pgsql'})
3316-
3317- # Trigger WSGI reloading
3318- for relid in relation_ids('wsgi'):
3319- relation_set({'wsgi_timestamp': time.time()}, relation_id=relid)
3320-
3321-def mongodb_relation_joined_changed():
3322- packages = ["python-mongoengine"]
3323- apt_get_install(packages)
3324-
3325- database = relation_get("database")
3326- if not database:
3327- return
3328-
3329- templ_vars = {
3330- 'db_database': database,
3331- 'db_host': relation_get("host"),
3332- }
3333-
3334- process_template('mongodb_engine.tmpl', templ_vars, settings_database_path % {'engine_name': 'mongodb'})
3335-
3336- run('chown -R %s:%s %s' % (wsgi_user,wsgi_group, working_dir))
3337-
3338- # Trigger WSGI reloading
3339- for relid in relation_ids('wsgi'):
3340- relation_set({'wsgi_timestamp': time.time()}, relation_id=relid)
3341-
3342-def mongodb_relation_broken():
3343- run('rm %s' % settings_database_path % {'engine_name': 'mongodb'})
3344-
3345- # Trigger WSGI reloading
3346- for relid in relation_ids('wsgi'):
3347- relation_set({'wsgi_timestamp': time.time()}, relation_id=relid)
3348-
3349-def wsgi_relation_joined_changed():
3350- relation_set({'working_dir':working_dir})
3351-
3352- for var in config_data:
3353- if var.startswith('wsgi_') or var in ['listen_ip', 'port']:
3354- relation_set({var: config_data[var]})
3355-
3356- if not config_data['python_path']:
3357- relation_set({'python_path': install_root})
3358-
3359- open_port(config_data['port'])
3360-
3361-def wsgi_relation_broken():
3362- close_port(config_data['port'])
3363-
3364-def cache_relation_joined_changed():
3365- os.environ['DJANGO_SETTINGS_MODULE'] = django_settings_modules
3366-
3367- packages = ["python-memcache"]
3368- apt_get_install(packages)
3369-
3370- host = relation_get("host")
3371- if not host:
3372- return
3373-
3374- templ_vars = {
3375- 'cache_engine': 'django.core.cache.backends.memcached.MemcachedCache',
3376- 'cache_host': relation_get("host"),
3377- 'cache_port': relation_get("port"),
3378- }
3379-
3380- process_template('cache.tmpl', templ_vars, settings_database_path % {'engine_name': 'memcache'})
3381-
3382- run('chown -R %s:%s %s' % (wsgi_user,wsgi_group, working_dir))
3383-
3384-
3385- # Trigger WSGI reloading
3386- for relid in relation_ids('wsgi'):
3387- relation_set({'wsgi_timestamp': time.time()}, relation_id=relid)
3388-
3389-def cache_relation_broken():
3390- run('rm %s' % settings_database_path % {'engine_name': 'memcache'})
3391-
3392- # Trigger WSGI reloading
3393- for relid in relation_ids('wsgi'):
3394- relation_set({'wsgi_timestamp': time.time()}, relation_id=relid)
3395-
3396-def website_relation_joined_changed():
3397- relation_set({'port': config_data["port"], 'hostname': get_unit_host()})
3398-
3399-def website_relation_broken():
3400- pass
3401-
3402-###############################################################################
3403-# Global variables
3404-###############################################################################
3405-config_data = config_get()
3406-juju_log(MSG_DEBUG, "got config: %s" % str(config_data))
3407-
3408-django_version = config_data['django_version']
3409-vcs = config_data['vcs']
3410-repos_url = config_data['repos_url']
3411-repos_username = config_data['repos_username']
3412-repos_password = config_data['repos_password']
3413-repos_branch = config_data['repos_branch']
3414-
3415-project_template_extension = config_data['project_template_extension']
3416-project_template_url = config_data['project_template_url']
3417-
3418-extra_deb_pkgs = config_data['additional_distro_packages']
3419-extra_pip_pkgs = config_data['additional_pip_packages']
3420-requirements_pip_files = config_data['requirements_pip_files']
3421-wsgi_user = config_data['wsgi_user']
3422-wsgi_group = config_data['wsgi_group']
3423-install_root = config_data['install_root']
3424-application_path = config_data['application_path']
3425-django_settings = config_data['django_settings']
3426-
3427-unit_name = os.environ['JUJU_UNIT_NAME'].split('/')[0]
3428-sanitized_unit_name = sanitize(unit_name)
3429-vcs_clone_dir = os.path.join(install_root, sanitized_unit_name)
3430-if application_path:
3431- working_dir = os.path.join(vcs_clone_dir, application_path)
3432-else:
3433- working_dir = vcs_clone_dir
3434-
3435-django_settings_modules = '.'.join([sanitized_unit_name, django_settings])
3436-django_run_dir = os.path.join(working_dir, "run/")
3437-django_logs_dir = os.path.join(working_dir, "logs/")
3438-settings_py_path = os.path.join(working_dir, 'settings.py')
3439-urls_py_path = os.path.join(working_dir, 'urls.py')
3440-settings_dir_path = os.path.join(working_dir, config_data["settings_dir_name"])
3441-urls_dir_path = os.path.join(working_dir, config_data["urls_dir_name"])
3442-settings_secret_path = os.path.join(working_dir, config_data["settings_secret_key_path"])
3443-settings_database_path = os.path.join(working_dir, config_data["settings_database_path"])
3444-hook_name = os.path.basename(sys.argv[0])
3445-
3446-###############################################################################
3447-# Main section
3448-###############################################################################
3449-def main():
3450- juju_log(MSG_INFO, "Running {} hook".format(hook_name))
3451- if hook_name == "install":
3452- install()
3453-
3454- elif hook_name == "start":
3455- start()
3456-
3457- elif hook_name == "stop":
3458- stop()
3459-
3460- elif hook_name == "config-changed":
3461- config_changed(config_data)
3462-
3463- elif hook_name == "upgrade-charm":
3464- upgrade()
3465- config_changed(config_data)
3466-
3467- elif hook_name in ["django-settings-relation-joined", "django-settings-relation-changed"]:
3468- django_settings_relation_joined_changed()
3469- config_changed(config_data)
3470-
3471- elif hook_name == "django-settings-relation-broken":
3472- django_settings_relation_broken()
3473- config_changed(config_data)
3474-
3475- elif hook_name in ["pgsql-relation-joined", "pgsql-relation-changed"]:
3476- pgsql_relation_joined_changed()
3477- config_changed(config_data)
3478-
3479- elif hook_name == "pgsql-relation-broken":
3480- pgsql_relation_broken()
3481- config_changed(config_data)
3482-
3483- elif hook_name in ["mongodb-relation-joined", "mongodb-relation-changed"]:
3484- mongodb_relation_joined_changed()
3485- config_changed(config_data)
3486-
3487- elif hook_name == "mongodb-relation-broken":
3488- mongodb_relation_broken()
3489- config_changed(config_data)
3490-
3491- elif hook_name in ["wsgi-relation-joined", "wsgi-relation-changed"]:
3492- wsgi_relation_joined_changed()
3493-
3494- elif hook_name == "wsgi-relation-broken":
3495- wsgi_relation_broken()
3496-
3497- elif hook_name in ["cache-relation-joined", "cache-relation-changed"]:
3498- cache_relation_joined_changed()
3499-
3500- elif hook_name == "cache-relation-broken":
3501- cache_relation_broken()
3502-
3503- elif hook_name in ["website-relation-joined", "website-relation-changed"]:
3504- website_relation_joined_changed()
3505-
3506- elif hook_name == "website-relation-broken":
3507- website_relation_broken()
3508-
3509-
3510- else:
3511- print "Unknown hook {}".format(hook_name)
3512- raise SystemExit(1)
3513-
3514-
3515-if __name__ == '__main__':
3516- raise SystemExit(main())
3517+ """Install ansible.
3518+
3519+ The hook() helper decorating this install function ensures that after this
3520+ function finishes, any tasks in the playbook tagged with install are
3521+ executed.
3522+ """
3523+ charmhelpers.contrib.ansible.install_ansible_support(from_ppa=True)
3524+
3525+
3526+if __name__ == "__main__":
3527+ hooks.execute(sys.argv)
3528
3529=== added symlink 'hooks/mysql-relation-broken'
3530=== target is u'hooks.py'
3531=== added symlink 'hooks/mysql-relation-changed'
3532=== target is u'hooks.py'
3533=== added symlink 'hooks/mysql-relation-joined'
3534=== target is u'hooks.py'
3535=== added symlink 'hooks/mysql-root-relation-broken'
3536=== target is u'hooks.py'
3537=== added symlink 'hooks/mysql-root-relation-changed'
3538=== target is u'hooks.py'
3539=== added symlink 'hooks/mysql-root-relation-joined'
3540=== target is u'hooks.py'
3541=== added symlink 'hooks/mysql-shared-relation-broken'
3542=== target is u'hooks.py'
3543=== added symlink 'hooks/mysql-shared-relation-changed'
3544=== target is u'hooks.py'
3545=== added symlink 'hooks/mysql-shared-relation-joined'
3546=== target is u'hooks.py'
3547=== added symlink 'hooks/redis-relation-broken'
3548=== target is u'hooks.py'
3549=== added symlink 'hooks/redis-relation-changed'
3550=== target is u'hooks.py'
3551=== added symlink 'hooks/redis-relation-joined'
3552=== target is u'hooks.py'
3553=== added file 'hooks/start'
3554--- hooks/start 1970-01-01 00:00:00 +0000
3555+++ hooks/start 2014-07-07 20:28:28 +0000
3556@@ -0,0 +1,1 @@
3557+#!/bin/sh
3558
3559=== removed symlink 'hooks/start'
3560=== target was u'hooks.py'
3561=== added file 'hooks/stop'
3562--- hooks/stop 1970-01-01 00:00:00 +0000
3563+++ hooks/stop 2014-07-07 20:28:28 +0000
3564@@ -0,0 +1,1 @@
3565+#!/bin/sh
3566
3567=== removed symlink 'hooks/stop'
3568=== target was u'hooks.py'
3569=== removed symlink 'hooks/website-relation-broken'
3570=== target was u'hooks.py'
3571=== removed symlink 'hooks/website-relation-changed'
3572=== target was u'hooks.py'
3573=== removed symlink 'hooks/website-relation-joined'
3574=== target was u'hooks.py'
3575=== modified file 'metadata.yaml'
3576--- metadata.yaml 2013-06-04 19:05:59 +0000
3577+++ metadata.yaml 2014-07-07 20:28:28 +0000
3578@@ -1,7 +1,7 @@
3579 name: python-django
3580 summary: High-level Python web development framework
3581 maintainer: Patrick Hetu <patrick.hetu@gmail.com>
3582-categories: ['databases', 'app-servers']
3583+categories: ['app-servers']
3584 description: |
3585 This charm will install Django. It can also install your Django
3586 project and his dependencies from either a template or from a
3587@@ -23,9 +23,25 @@
3588 pgsql:
3589 interface: pgsql
3590 optional: true
3591+ mysql:
3592+ interface: mysql
3593+ optional: true
3594+ mysql-root:
3595+ interface: mysql-root
3596+ mysql-shared:
3597+ interface: mysql-shared
3598 mongodb:
3599 interface: mongodb
3600 optional: true
3601+ redis:
3602+ interface: redis
3603+ optional: true
3604+ amqp:
3605+ interface: rabbitmq
3606+ optional: true
3607 cache:
3608 interface: memcache
3609 optional: true
3610+ lander-jenkins:
3611+ interface: lander-jenkins
3612+ optional: true
3613
3614=== added directory 'playbooks'
3615=== added directory 'playbooks/roles'
3616=== added file 'playbooks/roles/README.md'
3617--- playbooks/roles/README.md 1970-01-01 00:00:00 +0000
3618+++ playbooks/roles/README.md 2014-07-07 20:28:28 +0000
3619@@ -0,0 +1,4 @@
3620+charm-ansible-roles
3621+===================
3622+
3623+Reusable ansible roles for creating your juju charms with ansible
3624
3625=== added directory 'playbooks/roles/aptkit'
3626=== added directory 'playbooks/roles/aptkit/defaults'
3627=== added file 'playbooks/roles/aptkit/defaults/main.yml'
3628--- playbooks/roles/aptkit/defaults/main.yml 1970-01-01 00:00:00 +0000
3629+++ playbooks/roles/aptkit/defaults/main.yml 2014-07-07 20:28:28 +0000
3630@@ -0,0 +1,18 @@
3631+# List of extra package sources
3632+# YAML format.
3633+install_sources: ''
3634+
3635+# List of signing keys for install_sources package sources
3636+# YAML format.
3637+install_keys: ''
3638+
3639+# Comma separated extra packages to install.
3640+additional_distro_packages: ""
3641+
3642+# Comma separated relative paths to requirement files. Note that the charm
3643+# won't manually upgrade packages defined in this file.
3644+# Leave the variable to an empty string if you don't want the feature.
3645+requirements_apt_files: ""
3646+
3647+# where to search for requirements_apt_files
3648+requirements_apt_dir: ""
3649
3650=== added directory 'playbooks/roles/aptkit/tasks'
3651=== added file 'playbooks/roles/aptkit/tasks/main.yml'
3652--- playbooks/roles/aptkit/tasks/main.yml 1970-01-01 00:00:00 +0000
3653+++ playbooks/roles/aptkit/tasks/main.yml 2014-07-07 20:28:28 +0000
3654@@ -0,0 +1,31 @@
3655+- name: Add APT keys by url
3656+ apt_key: url="{{ item }}" state=present
3657+ with_lines: echo "{{ install_keys }}" | tr "," "\n" | awk "/^http/"
3658+ when: install_keys != ''
3659+
3660+- name: Add APT keys by file
3661+ apt_key: file="{{ item }}" state=present
3662+ with_lines: echo "{{ install_keys }}" | tr "," "\n" | awk "/^\//"
3663+ when: install_keys != ''
3664+
3665+- name: Add APT keys by data
3666+ apt_key: data="{{ item }}" state=present
3667+ with_lines: echo "{{ install_keys }}" | tr "," "\n" | awk "! /^\//" | awk "! /^http/"
3668+ when: install_keys != ''
3669+
3670+- name: Add APT respositories
3671+ apt_repository: repo="{{ item }}" state=present
3672+ with_items: install_sources
3673+ when: install_sources != ''
3674+
3675+- name: Install additional_distro_packages
3676+ apt: pkg="{{ item }}" state=latest update_cache=yes
3677+ with_items: additional_distro_packages.split(',')
3678+ register: result
3679+ until: result|success
3680+ retries: 24
3681+ delay: 10
3682+
3683+
3684+
3685+#requirements_apt_files
3686
3687=== added directory 'playbooks/roles/django-app'
3688=== added directory 'playbooks/roles/django-app/handlers'
3689=== added file 'playbooks/roles/django-app/handlers/main.yml'
3690--- playbooks/roles/django-app/handlers/main.yml 1970-01-01 00:00:00 +0000
3691+++ playbooks/roles/django-app/handlers/main.yml 2014-07-07 20:28:28 +0000
3692@@ -0,0 +1,7 @@
3693+- name: Restart wsgi
3694+ # Trigger the wsgi subordinate to restart by changing settings.
3695+ command: >
3696+ relation-set -r {{ item.key }}
3697+ wsgi_timestamp={{ ansible_date_time.iso8601_micro }}
3698+ when: relations['wsgi']
3699+ with_dict: relations['wsgi']
3700
3701=== added directory 'playbooks/roles/django-app/tasks'
3702=== added file 'playbooks/roles/django-app/tasks/main.yml'
3703--- playbooks/roles/django-app/tasks/main.yml 1970-01-01 00:00:00 +0000
3704+++ playbooks/roles/django-app/tasks/main.yml 2014-07-07 20:28:28 +0000
3705@@ -0,0 +1,67 @@
3706+# python_path={{ python_path_list }}
3707+# settings_dir_path={{ settings_dir_path }}
3708+# settings_module={{ settings_module }}
3709+# urls_dir_path={{ urls_dir_path }}
3710+# working_dir={{ working_dir }}
3711+# django_admin_cmd={{ django_admin_cmd.stdout }}
3712+# wsgi_user={{ wsgi_user }}
3713+# wsgi_group={{ wsgi_group }}
3714+
3715+- name: get pwd
3716+ command: pwd
3717+ register: pwd
3718+
3719+- name: Install distro dependencies
3720+ apt: pkg="{{ item }}" state=latest update_cache=yes
3721+ with_items: distro_packages
3722+ register: result
3723+ until: result|success
3724+ retries: 24
3725+ delay: 10
3726+ when: distro_packages is defined
3727+
3728+- name: Install pip dependencies
3729+ pip: name="{{ item }}" extra_args="{{ pip_extra_args }}"
3730+ with_items: pip_packages
3731+ when: pip_packages is defined
3732+
3733+- name: Install settings template
3734+ template:
3735+ owner="{{ settings_template.owner }}"
3736+ group="{{ settings_template.group }}"
3737+ mode="{{ settings_template.mode | default('0640') }}"
3738+ dest="{{ settings_template.dest | default(settings_dest) }}"
3739+ src="{{ item }}"
3740+ with_first_found:
3741+ - files:
3742+ - "{{ settings_template.src }}"
3743+ - "{{ relation_name }}.py.j2"
3744+ - settings.py.j2
3745+ paths:
3746+ - "{{ pwd.stdout }}/templates"
3747+ - "{{ pwd.stdout }}/roles/django-app/templates"
3748+ when: settings_template is defined
3749+
3750+- name: Install urls template
3751+ template:
3752+ owner="{{ urls_template.owner }}"
3753+ group="{{ urls_template.group }}"
3754+ mode="{{  urls_template.mode | default('0640') }}"
3755+ dest="{{ urls_template.dest | default(urls_dest) }}"
3756+ src="{{ item }}"
3757+ with_first_found:
3758+ - files:
3759+ - "{{ urls_template.src }}"
3760+ - "{{ relation_name }}.py.j2"
3761+ - "{{ relation_name }}.py.j2"
3762+ - urls.py.j2
3763+ paths:
3764+ - "{{ pwd.stdout }}/templates"
3765+ - "{{ pwd.stdout }}/roles/django-app/templates"
3766+ when: urls_template is defined
3767+
3768+- name: Run django commands
3769+ shell: PYTHONPATH={{ python_path_list }} {{ django_admin_cmd.stdout }} {{ item }} --settings {{ settings_module }} chdir={{ install_root }}
3770+ with_items: commands
3771+ ignore_errors: True
3772+ when: commands is defined
3773
3774=== added directory 'playbooks/roles/django-app/templates'
3775=== added file 'playbooks/roles/django-app/templates/amqp.py.j2'
3776--- playbooks/roles/django-app/templates/amqp.py.j2 1970-01-01 00:00:00 +0000
3777+++ playbooks/roles/django-app/templates/amqp.py.j2 2014-07-07 20:28:28 +0000
3778@@ -0,0 +1,13 @@
3779+import djcelery
3780+djcelery.setup_loader()
3781+
3782+# FIXME celery_amqp_vhost config
3783+BROKER_URL = 'amqp://{{ current_relation.username }}:{{ current_relation.password }}@{{ current_relation.hostname }}:5672/{{ sanitized_unit_name }}'
3784+
3785+#CELERY_RESULT_BACKEND='djcelery.backends.database:DatabaseBackend',
3786+
3787+CELERY_ALWAYS_EAGER = {{ celery_always_eager }}
3788+
3789+INSTALLED_APPS += (
3790+ 'djcelery',
3791+)
3792
3793=== added file 'playbooks/roles/django-app/templates/cache.py.j2'
3794--- playbooks/roles/django-app/templates/cache.py.j2 1970-01-01 00:00:00 +0000
3795+++ playbooks/roles/django-app/templates/cache.py.j2 2014-07-07 20:28:28 +0000
3796@@ -0,0 +1,10 @@
3797+#--------------------------------------------------------------
3798+# This file is managed by Juju; ANY CHANGES WILL BE OVERWRITTEN
3799+#--------------------------------------------------------------
3800+
3801+CACHES = {
3802+ 'default': {
3803+ 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
3804+ 'LOCATION': '{{ current_relation.host }}:{{ current_relation.port }}',
3805+ }
3806+}
3807
3808=== added file 'playbooks/roles/django-app/templates/cloudfiles.py.j2'
3809--- playbooks/roles/django-app/templates/cloudfiles.py.j2 1970-01-01 00:00:00 +0000
3810+++ playbooks/roles/django-app/templates/cloudfiles.py.j2 2014-07-07 20:28:28 +0000
3811@@ -0,0 +1,48 @@
3812+#--------------------------------------------------------------
3813+# This file is managed by Juju; ANY CHANGES WILL BE OVERWRITTEN
3814+#--------------------------------------------------------------
3815+
3816+import os
3817+
3818+PROJECT_ROOT = os.path.dirname(__file__)
3819+
3820+class SwiftAuthentication(object):
3821+ """Auth container to pass CloudFiles storage URL and token from
3822+ session.
3823+ """
3824+ def __init__(self, auth_url, username, password, tenant_id):
3825+ self.auth_url = auth_url
3826+ self.username = username
3827+ self.password = password
3828+ self.tenant_id = tenant_id
3829+
3830+ def authenticate(self):
3831+ from keystoneclient.v2_0 import client as ksclient
3832+ _ksclient = ksclient.Client(username=self.username,
3833+ password=self.password,
3834+ tenant_id=self.tenant_id,
3835+ auth_url=self.auth_url)
3836+ endpoint = _ksclient.service_catalog.url_for(service_type='object-store',
3837+ endpoint_type='publicURL')
3838+
3839+ return (endpoint, '', _ksclient.auth_token)
3840+
3841+auth = SwiftAuthentication('$SWIFT_AUTH_URL', '$SWIFT_USERNAME', '$SWIFT_PASSWORD', '$SWIFT_TENANTID')
3842+
3843+MEDIA_URL = "$SWIFT_ENDPOINT_URL/$SWIFT_VERSION/$SWIFT_TENANTID/$SWIFT_CONTAINER_NAME/uploads/"
3844+
3845+ADMIN_MEDIA_PREFIX = '$SWIFT_ENDPOINT_URL/$SWIFT_VERSION/$SWIFT_TENANTID/$SWIFT_CONTAINER_NAME/static/admin/'
3846+
3847+STATICFILES_URL = '$SWIFT_ENDPOINT_URL/$SWIFT_VERSION/$SWIFT_TENANTID/$SWIFT_CONTAINER_NAME/static/'
3848+STATIC_URL = STATICFILES_URL
3849+
3850+DEFAULT_FILE_STORAGE = 'cumulus.storage.CloudFilesStorage'
3851+STATICFILES_STORAGE = 'cumulus.storage.CloudFilesStaticStorage'
3852+
3853+CUMULUS = {
3854+ 'CONNECTION_ARGS': {'auth' : auth},
3855+ 'CONTAINER': '$SWIFT_CONTAINER_NAME'
3856+}
3857+
3858+COMPRESS_STORAGE = "cumulus.storage.CachedCloudFilesStaticStorage"
3859+
3860
3861=== added file 'playbooks/roles/django-app/templates/mongodb.py.j2'
3862--- playbooks/roles/django-app/templates/mongodb.py.j2 1970-01-01 00:00:00 +0000
3863+++ playbooks/roles/django-app/templates/mongodb.py.j2 2014-07-07 20:28:28 +0000
3864@@ -0,0 +1,8 @@
3865+#--------------------------------------------------------------
3866+# This file is managed by Juju; ANY CHANGES WILL BE OVERWRITTEN
3867+#--------------------------------------------------------------
3868+
3869+MONGO_DB = {
3870+ 'host': '{{ current_relation.host }}',
3871+ 'port': {{ current_relation.port }},
3872+}
3873
3874=== added file 'playbooks/roles/django-app/templates/mysql.py.j2'
3875--- playbooks/roles/django-app/templates/mysql.py.j2 1970-01-01 00:00:00 +0000
3876+++ playbooks/roles/django-app/templates/mysql.py.j2 2014-07-07 20:28:28 +0000
3877@@ -0,0 +1,18 @@
3878+#--------------------------------------------------------------
3879+# This file is managed by Juju; ANY CHANGES WILL BE OVERWRITTEN
3880+#--------------------------------------------------------------
3881+
3882+DATABASES = {
3883+ "default": {
3884+ "ENGINE": 'django.db.backends.mysql',
3885+ "NAME": '{{ current_relation.database }}',
3886+ "USER": '{{ current_relation.user }}',
3887+ "PASSWORD": '{{ current_relation.password }}',
3888+ "HOST": '{{ current_relation.host }}',
3889+ "PORT": '',
3890+ }
3891+}
3892+
3893+# Backward compatibility
3894+DATABASE_ENGINE=DATABASES
3895+
3896
3897=== added file 'playbooks/roles/django-app/templates/pgsql.py.j2'
3898--- playbooks/roles/django-app/templates/pgsql.py.j2 1970-01-01 00:00:00 +0000
3899+++ playbooks/roles/django-app/templates/pgsql.py.j2 2014-07-07 20:28:28 +0000
3900@@ -0,0 +1,19 @@
3901+#--------------------------------------------------------------
3902+# This file is managed by Juju; ANY CHANGES WILL BE OVERWRITTEN
3903+#--------------------------------------------------------------
3904+
3905+DATABASES = {
3906+ "default": {
3907+ "ENGINE": 'django.db.backends.postgresql_psycopg2',
3908+ "NAME": '{{ current_relation.database }}',
3909+ "USER": '{{ current_relation.user }}',
3910+ "PASSWORD": '{{ current_relation.password }}',
3911+ "HOST": '{{ current_relation.host }}',
3912+ "PORT": '',
3913+ "OPTIONS": {'autocommit': True},
3914+ }
3915+}
3916+
3917+# Backward compatibility
3918+DATABASE_ENGINE=DATABASES
3919+
3920
3921=== added file 'playbooks/roles/django-app/templates/redis.py.j2'
3922--- playbooks/roles/django-app/templates/redis.py.j2 1970-01-01 00:00:00 +0000
3923+++ playbooks/roles/django-app/templates/redis.py.j2 2014-07-07 20:28:28 +0000
3924@@ -0,0 +1,9 @@
3925+#--------------------------------------------------------------
3926+# This file is managed by Juju; ANY CHANGES WILL BE OVERWRITTEN
3927+#--------------------------------------------------------------
3928+
3929+REDIS = {
3930+ 'host': '{{ current_relation.hostname }}',
3931+ 'port': {{ current_relation.port }},
3932+ 'db': 0,
3933+}
3934
3935=== added directory 'playbooks/roles/django-app/vars'
3936=== added file 'playbooks/roles/django-app/vars/main.yml'
3937--- playbooks/roles/django-app/vars/main.yml 1970-01-01 00:00:00 +0000
3938+++ playbooks/roles/django-app/vars/main.yml 2014-07-07 20:28:28 +0000
3939@@ -0,0 +1,2 @@
3940+settings_dest: "{{ settings_template.settings_dir_path | default(working_dir) }}/{{ settings_template.priority | default('60') }}-{{ relation_name | default('unknown') }}.py"
3941+urls_dest: "{{ urls_template.urls_dir_path | default(working_dir) }}/{{ urls_template.priority | default('60') }}-{{ relation_name | default('unknown') }}.py"
3942
3943=== added directory 'playbooks/roles/django-project'
3944=== added directory 'playbooks/roles/django-project/defaults'
3945=== added file 'playbooks/roles/django-project/defaults/main.yml'
3946--- playbooks/roles/django-project/defaults/main.yml 1970-01-01 00:00:00 +0000
3947+++ playbooks/roles/django-project/defaults/main.yml 2014-07-07 20:28:28 +0000
3948@@ -0,0 +1,64 @@
3949+# The web site secret key. Leave empty will generate one.
3950+# NOTE: You **NEED** to set this in a multi-units architecture or you will
3951+# have some trouble.
3952+site_secret_key: ''
3953+
3954+# Version or origin from which to install. May be one of the following:
3955+# distro (default), ppa: somecustom/ppa, a deb url sources entry or
3956+# a valid pip line like 'Django' or 'Django==1.5' or a reposiroty url (without the -e).
3957+# Leaving it empty if you don't want the charm to install Django.
3958+django_version: "distro"
3959+
3960+# If not repository url is found, the charm will create a new project. This
3961+# option is the --template argument value for the startproject command
3962+# to use a custom project template.
3963+#
3964+# Django will also accept URLs (http, https, ftp) to compressed
3965+# archives with the app template files, downloading and extracting them on the fly.
3966+# For more informations see:
3967+# https: //docs.djangoproject.com/en/dev/ref/django-admin/#startproject-projectname-destination
3968+project_template_url: ""
3969+
3970+# When Django copies the project template files, it also renders certain
3971+# files through the template engine: the files whose extensions match the
3972+# --extension option (py by default) and the files whose names are passed with
3973+# the --name option.
3974+project_template_extension: ""
3975+
3976+# The root directory to checkout to.
3977+install_root: "/srv/"
3978+
3979+# The relative path to install_root where the manage.py
3980+# script is located.
3981+application_path: ""
3982+
3983+# base64 encoded string to hold configuration information for the unit.
3984+# The contents will be written to a file named
3985+# <install_root>/<unit>/unit_config
3986+# where <unit> is the location the branch is extracted to.
3987+unit_config: ""
3988+
3989+# The Python path to your Django settings module.
3990+# Leave it empty if your settings file is at the root of your repos.
3991+django_settings: ""
3992+
3993+# Enable the use of south migrations
3994+django_south: False
3995+
3996+# Version or origin from which to install. May be one of the following:
3997+# distro (default), ppa: somecustom/ppa, a deb url sources entry or
3998+# a valid pip line like 'South' or 'South==0.8.4' or a reposiroty url (without the -e).
3999+# Leaving it empty if you don't want the charm to install South.
4000+django_south_version: "distro"
4001+
4002+# Enable disable settings.DEBUG for django
4003+django_debug: False
4004+
4005+# A space separated list for settings.ALLOWED_HOSTS in django. Default
4006+# value will be the hostname, fully-qualified name, and public IP.
4007+django_allowed_hosts: ""
4008+
4009+# Allows setting up extra settings.* values for Django. Acceptable
4010+# values are limited to comma delimited key-value pairs like:
4011+# SETTING_X=foo, SETTING_Y=bar
4012+django_extra_settings: ""
4013
4014=== added directory 'playbooks/roles/django-project/tasks'
4015=== added file 'playbooks/roles/django-project/tasks/dynamic_vars.yml'
4016--- playbooks/roles/django-project/tasks/dynamic_vars.yml 1970-01-01 00:00:00 +0000
4017+++ playbooks/roles/django-project/tasks/dynamic_vars.yml 2014-07-07 20:28:28 +0000
4018@@ -0,0 +1,45 @@
4019+- name: set sanitized_unit_name
4020+ set_fact: sanitized_unit_name="{{ local_unit | regex_replace('^(.*)/.*$', '\\1') | regex_replace('-', '_') }}"
4021+
4022+- name: get relation name
4023+ set_fact: relation_name="{{ ansible_env.JUJU_RELATION | regex_replace('(.*)-relation-.*','\\\\1') }}"
4024+ when: ansible_env.JUJU_RELATION is defined
4025+
4026+- name: set working_dir if application_path != ''
4027+ set_fact: working_dir="{{ vcs_clone_dir }}/{{ application_path }}"
4028+ when: application_path != ''
4029+
4030+- name: set working_dir if application_path == ''
4031+ set_fact: working_dir="{{ vcs_clone_dir }}"
4032+ when: application_path == ''
4033+
4034+- name: set python_path_list if python_path != ''
4035+ set_fact: python_path_list="{{ working_dir }}/../:{{ python_path }}"
4036+ when: python_path != ''
4037+
4038+- name: set python_path_list if python_path == ''
4039+ set_fact: python_path_list="{{ working_dir }}/../"
4040+ when: python_path == ''
4041+
4042+- name: set settings_module if django_settings != ''
4043+ set_fact: settings_module="{{ django_settings }}"
4044+ when: django_settings != ''
4045+
4046+- name: set settings_module if django_settings == ''
4047+ set_fact: settings_module="{{ working_dir | basename }}.settings"
4048+ when: django_settings == ''
4049+
4050+- name: Generate the secret key if none is provided
4051+ command: pwgen -s 51 1
4052+ register: pwgen_site_secret_key
4053+ when: site_secret_key == ''
4054+
4055+- name: set site_secret_key with pwgen_site_secret_key
4056+ set_fact: site_secret_key="{{ pwgen_site_secret_key.stdout }}"
4057+ when: site_secret_key == ''
4058+
4059+- name: Found the django-admin script
4060+ shell: command -v /usr/bin/django-admin || command -v /usr/local/bin/django-admin.py
4061+ register: django_admin_cmd
4062+ ignore_errors: True
4063+
4064
4065=== added file 'playbooks/roles/django-project/tasks/main.yml'
4066--- playbooks/roles/django-project/tasks/main.yml 1970-01-01 00:00:00 +0000
4067+++ playbooks/roles/django-project/tasks/main.yml 2014-07-07 20:28:28 +0000
4068@@ -0,0 +1,42 @@
4069+---
4070+
4071+# Django
4072+- name: Install the Django debian package
4073+ apt: pkg=python-django state=latest update_cache=yes
4074+ when: django_version == 'distro'
4075+
4076+# or
4077+
4078+- name: Install Django pip package
4079+ pip: name="{{ django_version }}" state=latest
4080+ when: django_version != 'distro' and django_version != ''
4081+
4082+
4083+# South
4084+- name: Install the South debian package
4085+ apt: pkg=python-django-south state=latest update_cache=yes
4086+ when: django_south_version == 'distro'
4087+
4088+# or
4089+
4090+- name: Install the South pip package
4091+ pip: name="{{ django_south_version }}" state=latest
4092+ when: django_south_version != 'distro' and django_south_version != ''
4093+
4094+- include: dynamic_vars.yml
4095+
4096+# Django startproject for version >= 1.4
4097+- name: Create a Django project
4098+ shell: PYTHONPATH={{ python_path_list }} {{ django_admin_cmd.stdout }} startproject {{ sanitized_unit_name }} {{ install_root }} --template {{ project_template_url }} --extension {{ project_template_extension }}
4099+ when: vcs == ''
4100+ register: test_install_root
4101+ ignore_errors: True
4102+
4103+# Django startproject for version <= 1.3
4104+- name: Create a Django project
4105+ shell: PYTHONPATH={{ python_path_list }} {{ django_admin_cmd.stdout }} startproject {{ sanitized_unit_name }}
4106+ args:
4107+ chdir: "{{ install_root }}"
4108+ when: test_install_root|failed and vcs == ''
4109+
4110+
4111
4112=== added directory 'playbooks/roles/django-project/vars'
4113=== added file 'playbooks/roles/django-project/vars/main.yml'
4114--- playbooks/roles/django-project/vars/main.yml 1970-01-01 00:00:00 +0000
4115+++ playbooks/roles/django-project/vars/main.yml 2014-07-07 20:28:28 +0000
4116@@ -0,0 +1,3 @@
4117+vcs_clone_dir: "{{ install_root }}/{{ sanitized_unit_name }}"
4118+urls_dir_path: "{{ working_dir }}/{{ urls_injection_path | dirname }}/{{ urls_dir_name }}"
4119+settings_dir_path: "{{ working_dir }}/{{ settings_injection_path | dirname }}/{{ settings_dir_name }}"
4120
4121=== added directory 'playbooks/roles/django-settings-injection'
4122=== added directory 'playbooks/roles/django-settings-injection/handlers'
4123=== added file 'playbooks/roles/django-settings-injection/handlers/main.yml'
4124--- playbooks/roles/django-settings-injection/handlers/main.yml 1970-01-01 00:00:00 +0000
4125+++ playbooks/roles/django-settings-injection/handlers/main.yml 2014-07-07 20:28:28 +0000
4126@@ -0,0 +1,16 @@
4127+- name: Restart wsgi
4128+ # Trigger the wsgi subordinate to restart by changing settings.
4129+ command: >
4130+ relation-set -r {{ item.key }}
4131+ wsgi_timestamp={{ ansible_date_time.iso8601_micro }}
4132+ when: relations['wsgi']
4133+ with_dict: relations['wsgi']
4134+
4135+- name: syncdb
4136+ shell: PYTHONPATH={{ python_path_list }} {{ django_admin_cmd.stdout }} syncdb --noinput --settings {{ settings_module }} chdir={{ install_root }}
4137+ ignore_errors: True
4138+
4139+- name: migrate
4140+ shell: PYTHONPATH={{ python_path_list }} {{ django_admin_cmd.stdout }} migrate --settings {{ settings_module }} chdir={{ install_root }}
4141+ when: django_south
4142+ ignore_errors: True
4143
4144=== added directory 'playbooks/roles/django-settings-injection/tasks'
4145=== added file 'playbooks/roles/django-settings-injection/tasks/main.yml'
4146--- playbooks/roles/django-settings-injection/tasks/main.yml 1970-01-01 00:00:00 +0000
4147+++ playbooks/roles/django-settings-injection/tasks/main.yml 2014-07-07 20:28:28 +0000
4148@@ -0,0 +1,73 @@
4149+---
4150+
4151+# settings injection
4152+# FIXME see: https://github.com/ansible/ansible/issues/5679
4153+- name: Make conf directory for injections
4154+ command: mkdir {{ working_dir }}/{{ settings_injection_path | dirname }}/{{ settings_dir_name }} creates={{ working_dir }}/{{ settings_injection_path | dirname }}/{{ settings_dir_name }}
4155+ when: settings_dir_name != ''
4156+
4157+- name: Test for settings injections
4158+ command: grep "# The following is the import code for the settings directory injected by Juju" {{ settings_py_path }} chdir={{ working_dir }}/{{ settings_injection_path | dirname }}/{{ settings_dir_name }}
4159+ register: result_settings_injections
4160+ ignore_errors: True
4161+ when: settings_dir_name != ''
4162+
4163+- name: Create settings injection template
4164+ template: dest=/tmp/conf_injection.py src=conf_injection.py.j2
4165+ when: result_settings_injections|failed and settings_dir_name != ''
4166+
4167+- name: Inject settings
4168+ shell: cat /tmp/conf_injection.py >> {{ settings_py_path }} chdir={{ working_dir }}/{{ settings_injection_path | dirname }}/{{ settings_dir_name }}
4169+ when: result_settings_injections|failed
4170+
4171+- name: remove settings temporary template
4172+ command: rm /tmp/conf_injection.py
4173+ when: result_settings_injections|failed and settings_dir_name != ''
4174+
4175+# url injection
4176+# FIXME see: https://github.com/ansible/ansible/issues/5679
4177+- name: Make urls directory for injections
4178+ command: mkdir {{ working_dir }}/{{ urls_injection_path | dirname }}/{{ urls_dir_name }} creates={{ working_dir }}/{{ urls_injection_path | dirname }}/{{ urls_dir_name }}
4179+ when: urls_dir_name != ''
4180+
4181+- name: Test for urls injections
4182+ command: grep "# The following is the import code for the urls directory injected by Juju" {{ urls_py_path }} chdir={{ working_dir }}/{{ urls_injection_path | dirname }}/{{ urls_dir_name }}
4183+ register: result_urls_injections
4184+ ignore_errors: True
4185+ when: urls_dir_name != ''
4186+
4187+- name: Create urls injection template
4188+ template: dest=/tmp/urls_injection.py src=urls_injection.py.j2
4189+ when: result_urls_injections|failed and urls_dir_name != ''
4190+
4191+- name: Inject urls
4192+ shell: cat /tmp/urls_injection.py >> {{ urls_py_path }} chdir={{ working_dir }}/{{ urls_injection_path | dirname }}/{{ urls_dir_name }}
4193+ when: result_urls_injections|failed and urls_dir_name != ''
4194+
4195+- name: remove urls temporary template
4196+ command: rm /tmp/urls_injection.py
4197+ when: result_urls_injections|failed and urls_dir_name != ''
4198+
4199+- name: django-settings relation
4200+ command: >
4201+ relation-set
4202+ python_path={{ python_path_list }}
4203+ settings_dir_path={{ settings_dir_path }}
4204+ settings_module={{ settings_module }}
4205+ urls_dir_path={{ urls_dir_path }}
4206+ working_dir={{ working_dir }}
4207+ django_admin_cmd={{ django_admin_cmd.stdout }}
4208+ django_uid={{ django_uid }}
4209+ django_gid={{ django_gid }}
4210+ ignore_errors: True
4211+ when: django_admin_cmd.stdout is defined
4212+
4213+
4214+# permissions
4215+# FIXME use sudo django_uid
4216+- name: Set the right permissions for working_dir
4217+ file:
4218+ path={{ vcs_clone_dir }}
4219+ owner={{ django_uid }}
4220+ group={{ django_gid }}
4221+ recurse=yes state=directory
4222
4223=== added directory 'playbooks/roles/django-settings-injection/templates'
4224=== added file 'playbooks/roles/django-settings-injection/templates/allowed_hosts.py.j2'
4225--- playbooks/roles/django-settings-injection/templates/allowed_hosts.py.j2 1970-01-01 00:00:00 +0000
4226+++ playbooks/roles/django-settings-injection/templates/allowed_hosts.py.j2 2014-07-07 20:28:28 +0000
4227@@ -0,0 +1,9 @@
4228+#--------------------------------------------------------------
4229+# This file is managed by Juju; ANY CHANGES WILL BE OVERWRITTEN
4230+#--------------------------------------------------------------
4231+
4232+ALLOWED_HOSTS = [
4233+{% for allowed_hosts in django_allowed_hosts.split(',') %}
4234+ '{{ allowed_hosts }}',
4235+{% endfor %}
4236+]
4237
4238=== added file 'playbooks/roles/django-settings-injection/templates/amqp_celery.py.j2'
4239--- playbooks/roles/django-settings-injection/templates/amqp_celery.py.j2 1970-01-01 00:00:00 +0000
4240+++ playbooks/roles/django-settings-injection/templates/amqp_celery.py.j2 2014-07-07 20:28:28 +0000
4241@@ -0,0 +1,13 @@
4242+import djcelery
4243+djcelery.setup_loader()
4244+
4245+# FIXME celery_amqp_vhost config
4246+BROKER_URL = 'amqp://{{ current_relation.username }}:{{ current_relation.password }}@{{ current_relation.hostname }}:5672/{{ sanitized_unit_name }}'
4247+
4248+#CELERY_RESULT_BACKEND='djcelery.backends.database:DatabaseBackend',
4249+
4250+CELERY_ALWAYS_EAGER = {{ celery_always_eager }}
4251+
4252+INSTALLED_APPS += (
4253+ 'djcelery',
4254+)
4255
4256=== added file 'playbooks/roles/django-settings-injection/templates/cache.py.j2'
4257--- playbooks/roles/django-settings-injection/templates/cache.py.j2 1970-01-01 00:00:00 +0000
4258+++ playbooks/roles/django-settings-injection/templates/cache.py.j2 2014-07-07 20:28:28 +0000
4259@@ -0,0 +1,10 @@
4260+#--------------------------------------------------------------
4261+# This file is managed by Juju; ANY CHANGES WILL BE OVERWRITTEN
4262+#--------------------------------------------------------------
4263+
4264+CACHES = {
4265+ 'default': {
4266+ 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
4267+ 'LOCATION': '{{ current_relation.host }}:{{ current_relation.port }}',
4268+ }
4269+}
4270
4271=== added file 'playbooks/roles/django-settings-injection/templates/cloudfiles.py.j2'
4272--- playbooks/roles/django-settings-injection/templates/cloudfiles.py.j2 1970-01-01 00:00:00 +0000
4273+++ playbooks/roles/django-settings-injection/templates/cloudfiles.py.j2 2014-07-07 20:28:28 +0000
4274@@ -0,0 +1,48 @@
4275+#--------------------------------------------------------------
4276+# This file is managed by Juju; ANY CHANGES WILL BE OVERWRITTEN
4277+#--------------------------------------------------------------
4278+
4279+import os
4280+
4281+PROJECT_ROOT = os.path.dirname(__file__)
4282+
4283+class SwiftAuthentication(object):
4284+ """Auth container to pass CloudFiles storage URL and token from
4285+ session.
4286+ """
4287+ def __init__(self, auth_url, username, password, tenant_id):
4288+ self.auth_url = auth_url
4289+ self.username = username
4290+ self.password = password
4291+ self.tenant_id = tenant_id
4292+
4293+ def authenticate(self):
4294+ from keystoneclient.v2_0 import client as ksclient
4295+ _ksclient = ksclient.Client(username=self.username,
4296+ password=self.password,
4297+ tenant_id=self.tenant_id,
4298+ auth_url=self.auth_url)
4299+ endpoint = _ksclient.service_catalog.url_for(service_type='object-store',
4300+ endpoint_type='publicURL')
4301+
4302+ return (endpoint, '', _ksclient.auth_token)
4303+
4304+auth = SwiftAuthentication('$SWIFT_AUTH_URL', '$SWIFT_USERNAME', '$SWIFT_PASSWORD', '$SWIFT_TENANTID')
4305+
4306+MEDIA_URL = "$SWIFT_ENDPOINT_URL/$SWIFT_VERSION/$SWIFT_TENANTID/$SWIFT_CONTAINER_NAME/uploads/"
4307+
4308+ADMIN_MEDIA_PREFIX = '$SWIFT_ENDPOINT_URL/$SWIFT_VERSION/$SWIFT_TENANTID/$SWIFT_CONTAINER_NAME/static/admin/'
4309+
4310+STATICFILES_URL = '$SWIFT_ENDPOINT_URL/$SWIFT_VERSION/$SWIFT_TENANTID/$SWIFT_CONTAINER_NAME/static/'
4311+STATIC_URL = STATICFILES_URL
4312+
4313+DEFAULT_FILE_STORAGE = 'cumulus.storage.CloudFilesStorage'
4314+STATICFILES_STORAGE = 'cumulus.storage.CloudFilesStaticStorage'
4315+
4316+CUMULUS = {
4317+ 'CONNECTION_ARGS': {'auth' : auth},
4318+ 'CONTAINER': '$SWIFT_CONTAINER_NAME'
4319+}
4320+
4321+COMPRESS_STORAGE = "cumulus.storage.CachedCloudFilesStaticStorage"
4322+
4323
4324=== added file 'playbooks/roles/django-settings-injection/templates/conf_injection.py.j2'
4325--- playbooks/roles/django-settings-injection/templates/conf_injection.py.j2 1970-01-01 00:00:00 +0000
4326+++ playbooks/roles/django-settings-injection/templates/conf_injection.py.j2 2014-07-07 20:28:28 +0000
4327@@ -0,0 +1,18 @@
4328+#------------------------------------------------------------------------------
4329+# The following is the import code for the settings directory injected by Juju
4330+# Do not remove this comment
4331+#------------------------------------------------------------------------------
4332+
4333+import glob
4334+from os.path import abspath, dirname, join
4335+
4336+PROJECT_DIR = abspath(dirname(__file__))
4337+
4338+conffiles = glob.glob(join(PROJECT_DIR, '{{ settings_dir_name }}', '*.py'))
4339+conffiles.sort()
4340+
4341+if not 'INSTALLED_APPS' in locals():
4342+ from django.conf.global_settings import *
4343+
4344+for f in conffiles:
4345+ exec(open(abspath(f)).read())
4346
4347=== added file 'playbooks/roles/django-settings-injection/templates/debug.py.j2'
4348--- playbooks/roles/django-settings-injection/templates/debug.py.j2 1970-01-01 00:00:00 +0000
4349+++ playbooks/roles/django-settings-injection/templates/debug.py.j2 2014-07-07 20:28:28 +0000
4350@@ -0,0 +1,5 @@
4351+#--------------------------------------------------------------
4352+# This file is managed by Juju; ANY CHANGES WILL BE OVERWRITTEN
4353+#--------------------------------------------------------------
4354+
4355+DEBUG = {{django_debug}}
4356
4357=== added file 'playbooks/roles/django-settings-injection/templates/extra-conf.py.j2'
4358--- playbooks/roles/django-settings-injection/templates/extra-conf.py.j2 1970-01-01 00:00:00 +0000
4359+++ playbooks/roles/django-settings-injection/templates/extra-conf.py.j2 2014-07-07 20:28:28 +0000
4360@@ -0,0 +1,7 @@
4361+#--------------------------------------------------------------
4362+# This file is managed by Juju; ANY CHANGES WILL BE OVERWRITTEN
4363+#--------------------------------------------------------------
4364+
4365+{% for p in django_extra_settings.split(',') %}
4366+{{p}}
4367+{% endfor %}
4368
4369=== added file 'playbooks/roles/django-settings-injection/templates/mongodb_engine.py.j2'
4370--- playbooks/roles/django-settings-injection/templates/mongodb_engine.py.j2 1970-01-01 00:00:00 +0000
4371+++ playbooks/roles/django-settings-injection/templates/mongodb_engine.py.j2 2014-07-07 20:28:28 +0000
4372@@ -0,0 +1,8 @@
4373+#--------------------------------------------------------------
4374+# This file is managed by Juju; ANY CHANGES WILL BE OVERWRITTEN
4375+#--------------------------------------------------------------
4376+
4377+MONGO_DB = {
4378+ 'host': '{{ current_relation.host }}',
4379+ 'port': {{ current_relation.port }},
4380+}
4381
4382=== added file 'playbooks/roles/django-settings-injection/templates/mysql_engine.py.j2'
4383--- playbooks/roles/django-settings-injection/templates/mysql_engine.py.j2 1970-01-01 00:00:00 +0000
4384+++ playbooks/roles/django-settings-injection/templates/mysql_engine.py.j2 2014-07-07 20:28:28 +0000
4385@@ -0,0 +1,18 @@
4386+#--------------------------------------------------------------
4387+# This file is managed by Juju; ANY CHANGES WILL BE OVERWRITTEN
4388+#--------------------------------------------------------------
4389+
4390+DATABASES = {
4391+ "default": {
4392+ "ENGINE": '',
4393+ "NAME": '{{ current_relation.database }}',
4394+ "USER": '{{ current_relation.user }}',
4395+ "PASSWORD": '{{ current_relation.password }}',
4396+ "HOST": '{{ current_relation.host }}',
4397+ "PORT": '',
4398+ }
4399+}
4400+
4401+# Backward compatibility
4402+DATABASE_ENGINE=DATABASES
4403+
4404
4405=== added file 'playbooks/roles/django-settings-injection/templates/pgsql_engine.py.j2'
4406--- playbooks/roles/django-settings-injection/templates/pgsql_engine.py.j2 1970-01-01 00:00:00 +0000
4407+++ playbooks/roles/django-settings-injection/templates/pgsql_engine.py.j2 2014-07-07 20:28:28 +0000
4408@@ -0,0 +1,19 @@
4409+#--------------------------------------------------------------
4410+# This file is managed by Juju; ANY CHANGES WILL BE OVERWRITTEN
4411+#--------------------------------------------------------------
4412+
4413+DATABASES = {
4414+ "default": {
4415+ "ENGINE": 'django.db.backends.postgresql_psycopg2',
4416+ "NAME": '{{ current_relation.database }}',
4417+ "USER": '{{ current_relation.user }}',
4418+ "PASSWORD": '{{ current_relation.password }}',
4419+ "HOST": '{{ current_relation.host }}',
4420+ "PORT": '',
4421+ "OPTIONS": {'autocommit': True},
4422+ }
4423+}
4424+
4425+# Backward compatibility
4426+DATABASE_ENGINE=DATABASES
4427+
4428
4429=== added file 'playbooks/roles/django-settings-injection/templates/redis_engine.py.j2'
4430--- playbooks/roles/django-settings-injection/templates/redis_engine.py.j2 1970-01-01 00:00:00 +0000
4431+++ playbooks/roles/django-settings-injection/templates/redis_engine.py.j2 2014-07-07 20:28:28 +0000
4432@@ -0,0 +1,9 @@
4433+#--------------------------------------------------------------
4434+# This file is managed by Juju; ANY CHANGES WILL BE OVERWRITTEN
4435+#--------------------------------------------------------------
4436+
4437+REDIS = {
4438+ 'host': '{{ current_relation.hostname }}',
4439+ 'port': {{ current_relation.port }},
4440+ 'db': 0,
4441+}
4442
4443=== added file 'playbooks/roles/django-settings-injection/templates/secret.py.j2'
4444--- playbooks/roles/django-settings-injection/templates/secret.py.j2 1970-01-01 00:00:00 +0000
4445+++ playbooks/roles/django-settings-injection/templates/secret.py.j2 2014-07-07 20:28:28 +0000
4446@@ -0,0 +1,5 @@
4447+#--------------------------------------------------------------
4448+# This file is managed by Juju; ANY CHANGES WILL BE OVERWRITTEN
4449+#--------------------------------------------------------------
4450+
4451+SECRET_KEY = '{{ site_secret_key }}'
4452
4453=== added file 'playbooks/roles/django-settings-injection/templates/urls_injection.py.j2'
4454--- playbooks/roles/django-settings-injection/templates/urls_injection.py.j2 1970-01-01 00:00:00 +0000
4455+++ playbooks/roles/django-settings-injection/templates/urls_injection.py.j2 2014-07-07 20:28:28 +0000
4456@@ -0,0 +1,18 @@
4457+#-------------------------------------------------------------------------
4458+# The following is the import code for the urls directory injected by Juju
4459+# Do not remove this comment
4460+#-------------------------------------------------------------------------
4461+
4462+import glob
4463+from os.path import abspath, dirname, join
4464+
4465+PROJECT_DIR = abspath(dirname(__file__))
4466+
4467+conffiles = glob.glob(join(PROJECT_DIR, '{{ urls_dir_name }}', '*.py'))
4468+conffiles.sort()
4469+
4470+if not 'urlpatterns' in locals():
4471+ urlpatterns = ()
4472+
4473+for f in conffiles:
4474+ exec(open(abspath(f)).read())
4475
4476=== added file 'playbooks/roles/django-settings-injection/templates/wsgi.py.j2'
4477--- playbooks/roles/django-settings-injection/templates/wsgi.py.j2 1970-01-01 00:00:00 +0000
4478+++ playbooks/roles/django-settings-injection/templates/wsgi.py.j2 2014-07-07 20:28:28 +0000
4479@@ -0,0 +1,12 @@
4480+#--------------------------------------------------------------
4481+# This file is managed by Juju; ANY CHANGES WILL BE OVERWRITTEN
4482+#--------------------------------------------------------------
4483+
4484+import os
4485+import sys
4486+
4487+os.environ['DJANGO_SETTINGS_MODULE'] = '{{ settings_module }}'
4488+
4489+import django.core.handlers.wsgi
4490+application = django.core.handlers.wsgi.WSGIHandler()
4491+
4492
4493=== added directory 'playbooks/roles/django-settings-injection/vars'
4494=== added file 'playbooks/roles/django-settings-injection/vars/main.yml'
4495--- playbooks/roles/django-settings-injection/vars/main.yml 1970-01-01 00:00:00 +0000
4496+++ playbooks/roles/django-settings-injection/vars/main.yml 2014-07-07 20:28:28 +0000
4497@@ -0,0 +1,2 @@
4498+settings_py_path: "{{ working_dir }}/{{ settings_injection_path }}"
4499+urls_py_path: "{{ working_dir }}/{{ urls_injection_path }}"
4500
4501=== added directory 'playbooks/roles/nrpe-external-master'
4502=== added directory 'playbooks/roles/nrpe-external-master/defaults'
4503=== added file 'playbooks/roles/nrpe-external-master/defaults/main.yml'
4504--- playbooks/roles/nrpe-external-master/defaults/main.yml 1970-01-01 00:00:00 +0000
4505+++ playbooks/roles/nrpe-external-master/defaults/main.yml 2014-07-07 20:28:28 +0000
4506@@ -0,0 +1,5 @@
4507+---
4508+service_context: juju
4509+unit_name: "{{ local_unit | replace('/', '-') }}"
4510+plugin_dir: /usr/lib/nagios/plugins
4511+service_description:
4512
4513=== added directory 'playbooks/roles/nrpe-external-master/tasks'
4514=== added file 'playbooks/roles/nrpe-external-master/tasks/main.yml'
4515--- playbooks/roles/nrpe-external-master/tasks/main.yml 1970-01-01 00:00:00 +0000
4516+++ playbooks/roles/nrpe-external-master/tasks/main.yml 2014-07-07 20:28:28 +0000
4517@@ -0,0 +1,25 @@
4518+- name: Write nagios check command config.
4519+ tags:
4520+ - nrpe-external-master-relation-changed
4521+ template:
4522+ src: "check_name.cfg.jinja2"
4523+ dest: "/etc/nagios/nrpe.d/{{ check_name }}.cfg"
4524+ owner: nagios
4525+ group: nagios
4526+ mode: 0644
4527+
4528+- name: Write nagios check service definition for export.
4529+ tags:
4530+ - nrpe-external-master-relation-changed
4531+ template:
4532+ src: "check_name_service_export.cfg.jinja2"
4533+ dest: "/var/lib/nagios/export/service__{{ service_context }}-{{ unit_name }}_{{ check_name }}.cfg"
4534+ owner: nagios
4535+ group: nagios
4536+ mode: 0644
4537+
4538+- name: Trigger nrpe-external-master-relation-changed to restart.
4539+ tags:
4540+ - nrpe-external-master-relation-changed
4541+ command: >
4542+ relation-set timestamp={{ ansible_date_time.iso8601_micro }}
4543
4544=== added directory 'playbooks/roles/nrpe-external-master/templates'
4545=== added file 'playbooks/roles/nrpe-external-master/templates/check_name.cfg.jinja2'
4546--- playbooks/roles/nrpe-external-master/templates/check_name.cfg.jinja2 1970-01-01 00:00:00 +0000
4547+++ playbooks/roles/nrpe-external-master/templates/check_name.cfg.jinja2 2014-07-07 20:28:28 +0000
4548@@ -0,0 +1,4 @@
4549+#---------------------------------------------------
4550+# This file is Juju managed
4551+#---------------------------------------------------
4552+command[{{ check_name }}]={{ plugin_dir }}/{{ check_name }} {{ check_params }}
4553
4554=== added file 'playbooks/roles/nrpe-external-master/templates/check_name_service_export.cfg.jinja2'
4555--- playbooks/roles/nrpe-external-master/templates/check_name_service_export.cfg.jinja2 1970-01-01 00:00:00 +0000
4556+++ playbooks/roles/nrpe-external-master/templates/check_name_service_export.cfg.jinja2 2014-07-07 20:28:28 +0000
4557@@ -0,0 +1,10 @@
4558+#---------------------------------------------------
4559+# This file is Juju managed
4560+#---------------------------------------------------
4561+define service {
4562+ use active-service
4563+ host_name {{ service_context }}-{{ unit_name }}
4564+ service_description {{ service_description }}
4565+ check_command check_nrpe!{{ check_name }}
4566+ servicegroups {{ service_context }}
4567+}
4568
4569=== added directory 'playbooks/roles/pip'
4570=== added directory 'playbooks/roles/pip/defaults'
4571=== added file 'playbooks/roles/pip/defaults/main.yml'
4572--- playbooks/roles/pip/defaults/main.yml 1970-01-01 00:00:00 +0000
4573+++ playbooks/roles/pip/defaults/main.yml 2014-07-07 20:28:28 +0000
4574@@ -0,0 +1,14 @@
4575+# Comma separated extra packages to install.
4576+additional_pip_packages: ""
4577+
4578+# Comma separated relative paths or urls to a requirement files. Note that the charm
4579+# won't manually upgrade packages defined in this file.
4580+# Leave the variable to an empty string if you don't want the feature.
4581+requirements_pip_files: ""
4582+
4583+# Where to search for requirements files
4584+requirements_pip_dir: ""
4585+
4586+# Extra arguments passed to pip.
4587+pip_extra_args: ""
4588+
4589
4590=== added directory 'playbooks/roles/pip/tasks'
4591=== added file 'playbooks/roles/pip/tasks/main.yml'
4592--- playbooks/roles/pip/tasks/main.yml 1970-01-01 00:00:00 +0000
4593+++ playbooks/roles/pip/tasks/main.yml 2014-07-07 20:28:28 +0000
4594@@ -0,0 +1,28 @@
4595+- name: Install roles dependencies
4596+ apt: pkg="{{ item }}" state=latest update_cache=yes
4597+ with_items:
4598+ - python-pip
4599+ - mercurial
4600+ - git-core
4601+ - subversion
4602+ - bzr
4603+ register: result
4604+ until: result|success
4605+ retries: 24
4606+ delay: 10
4607+
4608+- name: Install pip dependencies
4609+ pip: name="{{ item }}" extra_args="{{ pip_extra_args }}"
4610+ with_items: additional_pip_packages.split(',')
4611+ when: additional_pip_packages != ''
4612+
4613+- name: Install pip requirements
4614+ pip: requirements="{{ requirements_pip_dir }}/{{ item }}" extra_args="{{ pip_extra_args }}"
4615+ with_lines: echo "{{ requirements_pip_files }}" | tr "," "\n" | awk "! /^http/"
4616+ when: requirements_pip_files != ''
4617+
4618+- name: Install http pip requirements
4619+ pip: requirements="{{ item }}" extra_args="{{ pip_extra_args }}"
4620+ with_lines: echo "{{ requirements_pip_files }}" | tr "," "\n" | awk "/^http/"
4621+ when: requirements_pip_files != ''
4622+
4623
4624=== added directory 'playbooks/roles/unit-config'
4625=== added directory 'playbooks/roles/unit-config/tasks'
4626=== added file 'playbooks/roles/unit-config/tasks/main.yml'
4627--- playbooks/roles/unit-config/tasks/main.yml 1970-01-01 00:00:00 +0000
4628+++ playbooks/roles/unit-config/tasks/main.yml 2014-07-07 20:28:28 +0000
4629@@ -0,0 +1,4 @@
4630+# unit_config
4631+- name: Create unit_config file
4632+ shell: cat "{{ unit_config | b64decode }}" > {{ unit_config_path }}
4633+ when: unit_config != ''
4634
4635=== added directory 'playbooks/roles/upstart'
4636=== added directory 'playbooks/roles/upstart/default'
4637=== added file 'playbooks/roles/upstart/default/main.yml'
4638--- playbooks/roles/upstart/default/main.yml 1970-01-01 00:00:00 +0000
4639+++ playbooks/roles/upstart/default/main.yml 2014-07-07 20:28:28 +0000
4640@@ -0,0 +1,1 @@
4641+name: ""
4642
4643=== added directory 'playbooks/roles/upstart/tasks'
4644=== added file 'playbooks/roles/upstart/tasks/main.yml'
4645--- playbooks/roles/upstart/tasks/main.yml 1970-01-01 00:00:00 +0000
4646+++ playbooks/roles/upstart/tasks/main.yml 2014-07-07 20:28:28 +0000
4647@@ -0,0 +1,13 @@
4648+- name: Install upstart template
4649+ template:
4650+ src: "upstart.conf.j2"
4651+ dest: "/etc/init/{{ name }}.conf"
4652+ owner: "root"
4653+ group: "root"
4654+ mode: "640"
4655+ when: name is defined
4656+
4657+- name: Restart the service
4658+ command: service restart "{{ name }}"
4659+ when: name is defined
4660+
4661
4662=== added directory 'playbooks/roles/upstart/templates'
4663=== added file 'playbooks/roles/upstart/templates/upstart.conf.j2'
4664--- playbooks/roles/upstart/templates/upstart.conf.j2 1970-01-01 00:00:00 +0000
4665+++ playbooks/roles/upstart/templates/upstart.conf.j2 2014-07-07 20:28:28 +0000
4666@@ -0,0 +1,47 @@
4667+#--------------------------------------------------------------
4668+# This file is managed by Juju; ANY CHANGES WILL BE OVERWRITTEN
4669+#--------------------------------------------------------------
4670+
4671+#FIXME
4672+
4673+description "Gunicorn daemon for the {{ project_name }} project"
4674+
4675+start on (local-filesystems and net-device-up IFACE=eth0)
4676+stop on runlevel [!12345]
4677+
4678+# If the process quits unexpectadly trigger a respawn
4679+respawn
4680+respawn limit 10 5
4681+
4682+setuid {{ wsgi_user }}
4683+setgid {{ wsgi_group }}
4684+chdir {{ working_dir }}
4685+
4686+# This line can be removed and replace with the --pythonpath {{ python_path }} \
4687+# option with Gunicorn>1.17
4688+env PYTHONPATH={{ python_path }}
4689+{% for name, value in env_extra -%}
4690+env {{ name }}="{{ value }}"
4691+{% endfor %}
4692+
4693+exec gunicorn \
4694+ --name={{ project_name }} \
4695+ --workers={{ wsgi_workers }} \
4696+ --worker-class={{ wsgi_worker_class }} \
4697+ --worker-connections={{ wsgi_worker_connections }} \
4698+ --max-requests={{ wsgi_max_requests }} \
4699+ --backlog={{ wsgi_backlog }} \
4700+ --timeout={{ wsgi_timeout }} \
4701+ --keep-alive={{ wsgi_keep_alive }} \
4702+ --umask={{ wsgi_umask }} \
4703+ --bind={{ listen_ip }}:{{ port }} \
4704+ --log-file={{ wsgi_log_file }} \
4705+ --log-level={{ wsgi_log_level }} \
4706+ {%- if wsgi_access_logfile %}
4707+ --access-logfile={{ wsgi_access_logfile }} \
4708+ {%- endif %}
4709+ {%- if wsgi_access_logformat %}
4710+ --access-logformat={{ wsgi_access_logformat }} \
4711+ {%- endif %}
4712+ {{ wsgi_extra }} \
4713+ {{ wsgi_wsgi_file }}
4714
4715=== added directory 'playbooks/roles/vcs'
4716=== added directory 'playbooks/roles/vcs/defaults'
4717=== added file 'playbooks/roles/vcs/defaults/main.yml'
4718--- playbooks/roles/vcs/defaults/main.yml 1970-01-01 00:00:00 +0000
4719+++ playbooks/roles/vcs/defaults/main.yml 2014-07-07 20:28:28 +0000
4720@@ -0,0 +1,22 @@
4721+# The vcs software to use. Only hg, git, bzr, and svn are currently supported.
4722+vcs: ""
4723+
4724+# The vcs url to checkout.
4725+repos_url: ""
4726+
4727+# The repo branch to pull out from. If empty, it will pull out the
4728+# default branch or trunk (such as origin/master with git).
4729+# Can also be a changeset, revision number or even tag depending
4730+# on the chosen vcs.
4731+repos_branch: ""
4732+
4733+# The vcs user name.
4734+# Note: *Subversion only* settings. For other vcs use the repos_url for auth.
4735+repos_username: ""
4736+
4737+# The vcs password.
4738+# Note: *Subversion only* settings. For other vcs use the repos_url for auth.
4739+repos_password: ""
4740+
4741+# The destination directory of the checkout.
4742+vcs_clone_dir: ""
4743
4744=== added directory 'playbooks/roles/vcs/tasks'
4745=== added file 'playbooks/roles/vcs/tasks/main.yml'
4746--- playbooks/roles/vcs/tasks/main.yml 1970-01-01 00:00:00 +0000
4747+++ playbooks/roles/vcs/tasks/main.yml 2014-07-07 20:28:28 +0000
4748@@ -0,0 +1,44 @@
4749+- name: Install roles dependencies
4750+ apt: pkg="{{ item }}" state=latest update_cache=yes
4751+ with_items:
4752+ - mercurial
4753+ - git-core
4754+ - subversion
4755+ - bzr
4756+ register: result
4757+ until: result|success
4758+ retries: 24
4759+ delay: 10
4760+
4761+- name: get mercurial source
4762+ hg: repo={{ repos_url }} dest={{ vcs_clone_dir }}
4763+ when: vcs == 'hg' or vcs == 'mercurial' and not repos_branch
4764+
4765+- name: get bzr source
4766+ bzr: name={{ repos_url }} dest={{ vcs_clone_dir }}
4767+ when: vcs == 'bzr' and not repos_branch
4768+
4769+- name: get git source
4770+ git: repo={{ repos_url }} dest={{ vcs_clone_dir }}
4771+ when: vcs == 'git' and not repos_branch
4772+
4773+- name: get subversion source
4774+ subversion: repo={{ repos_url }} dest={{ vcs_clone_dir }} username={{ repos_username }} password={{ repos_password }}
4775+ when: vcs == 'svn' or vcs == 'subversion' and not repos_branch
4776+
4777+#VCS + Branch
4778+- name: get mercurial source with branch
4779+ hg: repo={{ repos_url }} dest={{ vcs_clone_dir }} revision={{ repos_branch }}
4780+ when: vcs == 'hg' or vcs == 'mercurial' and repos_branch
4781+
4782+- name: get bzr source
4783+ bzr: name={{ repos_url }} dest={{ vcs_clone_dir }} version={{ repos_branch }}
4784+ when: vcs == 'bzr' and repos_branch
4785+
4786+- name: get git source with branch
4787+ git: repo={{ repos_url }} dest={{ vcs_clone_dir }} version={{ repos_branch }}
4788+ when: vcs == 'git' and repos_branch
4789+
4790+- name: get subversion source
4791+ subversion: repo={{ repos_url }} dest={{ vcs_clone_dir }} username={{ repos_username }} password={{ repos_password }} revision={{ repos_branch }}
4792+ when: vcs == 'svn' or vcs == 'subversion' and repos_branch
4793
4794=== added directory 'playbooks/roles/wsgi-app'
4795=== added file 'playbooks/roles/wsgi-app/README'
4796--- playbooks/roles/wsgi-app/README 1970-01-01 00:00:00 +0000
4797+++ playbooks/roles/wsgi-app/README 2014-07-07 20:28:28 +0000
4798@@ -0,0 +1,29 @@
4799+The wsgi-app charm role
4800+=======================
4801+
4802+This ansible role can be included in your charm, enabling you to re-use
4803+existing tested charm functionality to deploy your wsgi application.
4804+
4805+To see an example of how this role can be used for a charm, see XXX.
4806+
4807+The role assumes that your charm defines the following items (as either
4808+charm config options, or as variables in your playbook):
4809+
4810+Required:
4811+ * app_label - A label to identify your app - a domain will be fine.
4812+ * code_archive - the relative name of your application code archive,
4813+ eg. "r25/my-app.bzip2", or "beta-test/my-app.bzip2".
4814+ * wsgi_application - the location of your wsgi application relative to
4815+ your code. This is passed to the wsgi provider. For example:
4816+ "myproject.wsgi:application".
4817+
4818+Optional:
4819+ * code_asset_uri - an optional uri from which the code_archive will be
4820+ sourced. Without this, it'll look for code_archive in the ${CHARM}/files
4821+ directory.
4822+ * current_symlink - an optional label that can be used for rolling upgrades.
4823+ By default this always points to the last installed code_archive. But you
4824+ can explicitly request a previously installed version of your code to be
4825+ used instead.
4826+ * listen_port - the port on which the wsgi provider should serve your app.
4827+ * env_extra - extra environment variables to pass to the wsgi-file provider.
4828
4829=== added directory 'playbooks/roles/wsgi-app/defaults'
4830=== added file 'playbooks/roles/wsgi-app/defaults/main.yml'
4831--- playbooks/roles/wsgi-app/defaults/main.yml 1970-01-01 00:00:00 +0000
4832+++ playbooks/roles/wsgi-app/defaults/main.yml 2014-07-07 20:28:28 +0000
4833@@ -0,0 +1,7 @@
4834+code_assets_uri: ''
4835+python_path: ''
4836+current_symlink: latest
4837+listen_port: 8080
4838+env_extra: ''
4839+wsgi_user: wsgi_user
4840+wsgi_group: wsgi_group
4841
4842=== added directory 'playbooks/roles/wsgi-app/handlers'
4843=== added file 'playbooks/roles/wsgi-app/handlers/main.yml'
4844--- playbooks/roles/wsgi-app/handlers/main.yml 1970-01-01 00:00:00 +0000
4845+++ playbooks/roles/wsgi-app/handlers/main.yml 2014-07-07 20:28:28 +0000
4846@@ -0,0 +1,7 @@
4847+- name: Restart wsgi
4848+ # Trigger the wsgi subordinate to restart by changing settings.
4849+ command: >
4850+ relation-set -r {{ item.key }}
4851+ timestamp={{ ansible_date_time.iso8601_micro }}
4852+ when: relations['wsgi-file']
4853+ with_dict: relations['wsgi-file']
4854
4855=== added directory 'playbooks/roles/wsgi-app/tasks'
4856=== added file 'playbooks/roles/wsgi-app/tasks/main.yml'
4857--- playbooks/roles/wsgi-app/tasks/main.yml 1970-01-01 00:00:00 +0000
4858+++ playbooks/roles/wsgi-app/tasks/main.yml 2014-07-07 20:28:28 +0000
4859@@ -0,0 +1,78 @@
4860+---
4861+#- include: setup-machine.yml
4862+#- include: setup-code.yml
4863+
4864+#- name: Symlink latest tarball of application code
4865+# tags:
4866+# - config-changed
4867+# file:
4868+# src: "{{ code_dir }}/{{ code_archive | dirname }}"
4869+# dest: "{{ code_dir }}/latest"
4870+# owner: "{{ wsgi_user }}"
4871+# group: "{{ wsgi_group }}"
4872+# state: link
4873+#
4874+#- name: Check whether the set current symlink exists.
4875+# tags:
4876+# - wsgi-file-relation-changed
4877+# - config-changed
4878+# stat: path={{ code_dir }}/{{ current_symlink }}
4879+# register: stat_current_symlink
4880+#- name: Fail if the configured current_symlink does not exist.
4881+# tags:
4882+# - wsgi-file-relation-changed
4883+# - config-changed
4884+# fail: msg="The configured current_symlink does not exist, {{ code_dir }}/{{ current_symlink }}"
4885+# when: stat_current_symlink.stat.exists == False
4886+#
4887+#- name: Update the current symlink.
4888+# tags:
4889+# - wsgi-file-relation-changed
4890+# - config-changed
4891+# file:
4892+# src: "{{ code_dir }}/{{ current_symlink }}"
4893+# dest: "{{ code_dir }}/current"
4894+# owner: "{{ wsgi_user }}"
4895+# group: "{{ wsgi_group }}"
4896+# state: link
4897+
4898+- name: Set wsgi details if relation defined
4899+ command: >
4900+ relation-set -r {{ item.key }}
4901+ project_name={{ app_label }}
4902+ working_dir={{ working_dir }}
4903+ python_path={{ python_path }}
4904+ wsgi_user={{ wsgi_user }}
4905+ wsgi_group={{ wsgi_group }}
4906+ port={{ listen_port }}
4907+ wsgi_wsgi_file={{ wsgi_application }}
4908+ env_extra='{{ env_extra }}'
4909+ timestamp={{ ansible_date_time.iso8601_micro }}
4910+ when: relations['wsgi']
4911+ with_dict: relations['wsgi']
4912+
4913+# wsgi_access_logfile={{ log_dir }}/{{ app_label }}-access.log
4914+# wsgi_extra=--error-logfile={{ log_dir }}/{{ app_label }}-error.log
4915+
4916+#- name: Set the website relation if defined.
4917+# tags:
4918+# - website-relation-changed
4919+# - config-changed
4920+# command: >
4921+# relation-set -r {{ item.key }}
4922+# hostname={{ ansible_default_ipv4.address }}
4923+# port={{ listen_port }}
4924+# when: relations['website']
4925+# with_dict: relations['website']
4926+#
4927+#- name: Manually set current symlink.
4928+# tags:
4929+# - set-current-symlink
4930+# file:
4931+# src: "{{ code_dir }}/{{ ansible_env.CURRENT_SYMLINK }}"
4932+# dest: "{{ code_dir }}/current"
4933+# owner: "{{ wsgi_user }}"
4934+# group: "{{ wsgi_group }}"
4935+# state: link
4936+# notify:
4937+# - Restart wsgi
4938
4939=== added file 'playbooks/roles/wsgi-app/tasks/setup-code.yml'
4940--- playbooks/roles/wsgi-app/tasks/setup-code.yml 1970-01-01 00:00:00 +0000
4941+++ playbooks/roles/wsgi-app/tasks/setup-code.yml 2014-07-07 20:28:28 +0000
4942@@ -0,0 +1,54 @@
4943+---
4944+- name: Copy code_archive from the charm if not using code_assets_uri
4945+ tags:
4946+ - config-changed
4947+ copy:
4948+ src: "{{ charm_dir }}/files/{{ code_archive }}"
4949+ dest: "{{ archives_dir }}/{{ code_archive }}"
4950+ force: no
4951+ owner: "{{ wsgi_user }}"
4952+ group: "{{ wsgi_group }}"
4953+ mode: 0644
4954+ when: code_assets_uri == ""
4955+
4956+- name: Download code tarball archive from the code assets uri
4957+ tags:
4958+ - config-changed
4959+ get_url:
4960+ url: "{{ code_assets_uri }}/{{ code_archive }}"
4961+ dest: "{{ archives_dir }}/{{ code_archive }}"
4962+ force: no
4963+ owner: "{{ wsgi_user }}"
4964+ group: "{{ wsgi_group }}"
4965+ mode: 0644
4966+ when: code_assets_uri != ""
4967+
4968+- name: Check if archive is already extracted
4969+ tags:
4970+ - config-changed
4971+ stat: path={{ current_code_dir }}/EXTRACTED
4972+ register: already_extracted
4973+
4974+- name: Extract built app sourcecode.
4975+ tags:
4976+ - config-changed
4977+ unarchive:
4978+ src: "{{ archives_dir }}/{{ code_archive }}"
4979+ dest: "{{ current_code_dir }}"
4980+ owner: "{{ wsgi_user }}"
4981+ group: "{{ wsgi_group }}"
4982+ when: already_extracted.stat.exists == False
4983+
4984+# The following is only necessary because the unarchived code files
4985+# won't have the correct user/group.
4986+- name: Set user/group for application directory.
4987+ tags:
4988+ - config-changed
4989+ file: path={{ code_dir }} state=directory owner={{ wsgi_user }} group={{ wsgi_group }} recurse=yes
4990+ when: already_extracted.stat.exists == False
4991+
4992+- name: Touch a file to ensure that we don't extract the same archive again.
4993+ command: /usr/bin/touch {{ current_code_dir }}/EXTRACTED
4994+ tags:
4995+ - config-changed
4996+ when: already_extracted.stat.exists == False
4997
4998=== added file 'playbooks/roles/wsgi-app/tasks/setup-machine.yml'
4999--- playbooks/roles/wsgi-app/tasks/setup-machine.yml 1970-01-01 00:00:00 +0000
5000+++ playbooks/roles/wsgi-app/tasks/setup-machine.yml 2014-07-07 20:28:28 +0000
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches

to all changes: