Merge lp:~patrick-hetu/charms/precise/python-django/charmhelpers into lp:~charmers/charms/precise/python-django/trunk
- Precise Pangolin (12.04)
- charmhelpers
- Merge into trunk
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Charles Butler (community) | Needs Fixing | ||
Review via email: mp+216351@code.launchpad.net |
Commit message
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
- 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
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
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
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://
ERROR
=======
ERROR: setUpModule (__main__)
-------
Traceback (most recent call last):
File "tests/
attempts=10, retry_unavailab
File "/home/
sys.
BrokenPipeError: [Errno 32] Broken pipe
-------
Ran 0 tests in 679.730s
FAILED (errors=1)
Exception ignored in: <_io.TextIOWrapper name='<stdout>' mode='w' encoding=
BrokenPipeError: [Errno 32] Broken pipe
2014-09-08 21:05:55,425 INFO Unavailable, retrying: http://
ERROR
=======
ERROR: test_app (01-versions.
Verify that the APP service is up.
-------
Traceback (most recent call last):
File "tests/
attempts=10, retry_unavailab
File "/home/
sys.
BrokenPipeError: [Errno 32] Broken pipe
-------
Ran 1 test in 1558.975s
FAILED (errors=1)
Exception ignored in: <_io.TextIOWrapper name='<stdout>' mode='w' encoding=
BrokenPipeError: [Errno 32] Broken pipe
Thanks again for the submission, I look forward to the next iteration and progress!
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
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 | |
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 |
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