Merge lp:~brad-marshall/charms/precise/python-moinmoin/python-rewrite-with-openid-fixes into lp:charms/python-moinmoin
- Precise Pangolin (12.04)
- python-rewrite-with-openid-fixes
- Merge into trunk
Proposed by
Brad Marshall
Status: | Merged |
---|---|
Merged at revision: | 8 |
Proposed branch: | lp:~brad-marshall/charms/precise/python-moinmoin/python-rewrite-with-openid-fixes |
Merge into: | lp:charms/python-moinmoin |
Diff against target: |
14878 lines (+13568/-295) 115 files modified
README.md (+0/-62) config.yaml (+0/-30) copyright (+0/-17) files/openidrp.py (+325/-0) files/openidrp_teams.py (+157/-0) files/teams.py (+398/-0) hooks/config-changed (+0/-54) hooks/hooks.py (+226/-0) hooks/install (+0/-19) hooks/start (+0/-4) hooks/stop (+0/-7) hooks/website-relation-joined (+0/-2) lib/charm-helpers/LICENSE.txt (+661/-0) lib/charm-helpers/MANIFEST.in (+6/-0) lib/charm-helpers/Makefile (+46/-0) lib/charm-helpers/README.test (+7/-0) lib/charm-helpers/README.txt (+8/-0) lib/charm-helpers/REVISION (+1/-0) lib/charm-helpers/VERSION (+1/-0) lib/charm-helpers/bin/README (+1/-0) lib/charm-helpers/bin/chlp (+7/-0) lib/charm-helpers/bin/contrib/charmsupport/charmsupport (+31/-0) lib/charm-helpers/bin/contrib/saltstack/salt-call (+11/-0) lib/charm-helpers/charmhelpers/cli/README.rst (+57/-0) lib/charm-helpers/charmhelpers/cli/__init__.py (+147/-0) lib/charm-helpers/charmhelpers/cli/commands.py (+2/-0) lib/charm-helpers/charmhelpers/cli/host.py (+14/-0) lib/charm-helpers/charmhelpers/contrib/ansible/__init__.py (+101/-0) lib/charm-helpers/charmhelpers/contrib/charmhelpers/IMPORT (+4/-0) lib/charm-helpers/charmhelpers/contrib/charmhelpers/__init__.py (+184/-0) lib/charm-helpers/charmhelpers/contrib/charmsupport/IMPORT (+14/-0) lib/charm-helpers/charmhelpers/contrib/charmsupport/nrpe.py (+218/-0) lib/charm-helpers/charmhelpers/contrib/charmsupport/volumes.py (+156/-0) lib/charm-helpers/charmhelpers/contrib/hahelpers/apache.py (+58/-0) lib/charm-helpers/charmhelpers/contrib/hahelpers/ceph.py (+294/-0) lib/charm-helpers/charmhelpers/contrib/hahelpers/cluster.py (+181/-0) lib/charm-helpers/charmhelpers/contrib/jujugui/IMPORT (+4/-0) lib/charm-helpers/charmhelpers/contrib/jujugui/utils.py (+602/-0) lib/charm-helpers/charmhelpers/contrib/network/ovs/__init__.py (+72/-0) lib/charm-helpers/charmhelpers/contrib/openstack/context.py (+294/-0) lib/charm-helpers/charmhelpers/contrib/openstack/templates/__init__.py (+2/-0) lib/charm-helpers/charmhelpers/contrib/openstack/templates/ceph.conf (+11/-0) lib/charm-helpers/charmhelpers/contrib/openstack/templates/haproxy.cfg (+37/-0) lib/charm-helpers/charmhelpers/contrib/openstack/templates/openstack_https_frontend (+23/-0) lib/charm-helpers/charmhelpers/contrib/openstack/templating.py (+261/-0) lib/charm-helpers/charmhelpers/contrib/openstack/utils.py (+276/-0) lib/charm-helpers/charmhelpers/contrib/saltstack/__init__.py (+149/-0) lib/charm-helpers/charmhelpers/contrib/ssl/__init__.py (+79/-0) lib/charm-helpers/charmhelpers/contrib/storage/linux/loopback.py (+62/-0) lib/charm-helpers/charmhelpers/contrib/storage/linux/lvm.py (+88/-0) lib/charm-helpers/charmhelpers/contrib/storage/linux/utils.py (+25/-0) lib/charm-helpers/charmhelpers/contrib/templating/pyformat.py (+13/-0) lib/charm-helpers/charmhelpers/core/hookenv.py (+340/-0) lib/charm-helpers/charmhelpers/core/host.py (+241/-0) lib/charm-helpers/charmhelpers/fetch/__init__.py (+209/-0) lib/charm-helpers/charmhelpers/fetch/archiveurl.py (+48/-0) lib/charm-helpers/charmhelpers/fetch/bzrurl.py (+49/-0) lib/charm-helpers/charmhelpers/payload/__init__.py (+1/-0) lib/charm-helpers/charmhelpers/payload/archive.py (+57/-0) lib/charm-helpers/charmhelpers/payload/execd.py (+50/-0) lib/charm-helpers/debian/compat (+1/-0) lib/charm-helpers/debian/control (+20/-0) lib/charm-helpers/debian/rules (+9/-0) lib/charm-helpers/debian/source/format (+1/-0) lib/charm-helpers/scripts/README (+1/-0) lib/charm-helpers/scripts/update-revno (+11/-0) lib/charm-helpers/setup.cfg (+4/-0) lib/charm-helpers/setup.py (+42/-0) lib/charm-helpers/tarmac_tests.sh (+6/-0) lib/charm-helpers/test_requirements.txt (+18/-0) lib/charm-helpers/tests/cli/test_cmdline.py (+189/-0) lib/charm-helpers/tests/cli/test_function_signature_analysis.py (+46/-0) lib/charm-helpers/tests/contrib/ansible/test_ansible.py (+134/-0) lib/charm-helpers/tests/contrib/charmhelpers/test_charmhelpers.py (+290/-0) lib/charm-helpers/tests/contrib/charmsupport/test_nrpe.py (+222/-0) lib/charm-helpers/tests/contrib/hahelpers/test_apache_utils.py (+44/-0) lib/charm-helpers/tests/contrib/hahelpers/test_ceph_utils.py (+131/-0) lib/charm-helpers/tests/contrib/hahelpers/test_cluster_utils.py (+277/-0) lib/charm-helpers/tests/contrib/jujugui/config/apache-ports.template (+2/-0) lib/charm-helpers/tests/contrib/jujugui/config/apache-site.template (+30/-0) lib/charm-helpers/tests/contrib/jujugui/config/config.js.template (+26/-0) lib/charm-helpers/tests/contrib/jujugui/config/haproxy.cfg.template (+48/-0) lib/charm-helpers/tests/contrib/jujugui/config/haproxy.conf (+22/-0) lib/charm-helpers/tests/contrib/jujugui/config/juju-api-agent.conf.template (+14/-0) lib/charm-helpers/tests/contrib/jujugui/config/juju-api-improv.conf.template (+12/-0) lib/charm-helpers/tests/contrib/jujugui/deploy.test (+232/-0) lib/charm-helpers/tests/contrib/jujugui/test_utils.py (+660/-0) lib/charm-helpers/tests/contrib/jujugui/unit.test (+11/-0) lib/charm-helpers/tests/contrib/network/test_ovs.py (+202/-0) lib/charm-helpers/tests/contrib/openstack/test_openstack_utils.py (+421/-0) lib/charm-helpers/tests/contrib/openstack/test_os_contexts.py (+473/-0) lib/charm-helpers/tests/contrib/openstack/test_os_templating.py (+232/-0) lib/charm-helpers/tests/contrib/saltstack/test_saltstates.py (+229/-0) lib/charm-helpers/tests/contrib/ssl/test_ssl.py (+60/-0) lib/charm-helpers/tests/contrib/storage/test_linux_storage_loopback.py (+75/-0) lib/charm-helpers/tests/contrib/storage/test_linux_storage_lvm.py (+70/-0) lib/charm-helpers/tests/contrib/storage/test_linux_storage_utils.py (+23/-0) lib/charm-helpers/tests/contrib/templating/test_pyformat.py (+36/-0) lib/charm-helpers/tests/core/test_hookenv.py (+797/-0) lib/charm-helpers/tests/core/test_host.py (+678/-0) lib/charm-helpers/tests/fetch/test_archiveurl.py (+89/-0) lib/charm-helpers/tests/fetch/test_bzrurl.py (+80/-0) lib/charm-helpers/tests/fetch/test_fetch.py (+407/-0) lib/charm-helpers/tests/payload/test_archive.py (+131/-0) lib/charm-helpers/tests/payload/test_execd.py (+151/-0) lib/charm-helpers/tests/tools/test_charm_helper_sync.py (+249/-0) lib/charm-helpers/tools/charm_helpers_sync/README (+114/-0) lib/charm-helpers/tools/charm_helpers_sync/charm_helpers_sync.py (+225/-0) lib/charm-helpers/tools/charm_helpers_sync/example-config.yaml (+14/-0) metadata.yaml (+0/-10) revision (+0/-1) templates/gunicorn.conf.tmpl (+0/-18) templates/logging.conf.tmpl (+0/-31) templates/wikiconfig.py.tmpl (+0/-30) templates/wsgi.py.tmpl (+0/-10) |
To merge this branch: | bzr merge lp:~brad-marshall/charms/precise/python-moinmoin/python-rewrite-with-openid-fixes |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Marco Ceppi (community) | Approve | ||
Review via email: mp+187672@code.launchpad.net |
Commit message
Description of the change
Rewrote the charm in python using charm-helper.
Added openid auth support, testing can be done using:
$ juju set python-moinmoin use_openid=True openidrp_
Then try logging into http://<instance>:8080/ with a user in your-openid-team.
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === added file 'README.md' |
2 | --- README.md 1970-01-01 00:00:00 +0000 |
3 | +++ README.md 2013-09-26 06:24:47 +0000 |
4 | @@ -0,0 +1,72 @@ |
5 | +# Overview |
6 | +Juju charm for python-moinmoin, charm author: Patrick Hetu <patrick@koumbit.org> |
7 | + |
8 | +# Deployment |
9 | +After a successful bootstrap, you can deploy the service without a configuration or alias with: |
10 | + |
11 | + juju deploy python-moinmoin |
12 | + juju expose python-moinmoin |
13 | + |
14 | +Though, it's recommended you create a yaml file with your parameters, for instance if you were to deploy the service as "mywiki", the configuration would look like: |
15 | + |
16 | + mywiki: |
17 | + wiki_name : "My MoinMoin wiki" |
18 | + admin_name: "Admin" |
19 | + languages: English French |
20 | + xapian_search: True |
21 | + port: 80 |
22 | + |
23 | +To then deploy the wiki, with pre-seeded configuration: |
24 | + |
25 | + juju deploy --config mywiki.yaml python-moinmoin mywiki |
26 | + juju expose mywiki |
27 | + |
28 | +# Configuration |
29 | +This service contains several configuration options for controlling the moinmoin install |
30 | + |
31 | +## wiki_name |
32 | +Set the name and title of the wiki |
33 | + |
34 | + juju set python_moinmoin wiki_name="My new wiki name" |
35 | + |
36 | +## admin_name |
37 | +Name of the admin account |
38 | + |
39 | + juju set python_moinmoin admin_name="Adminstrator" |
40 | + |
41 | +## languages |
42 | +List of languages to be installed on the wiki, space delimited |
43 | + |
44 | + juju set python_moinmoin languages English Spanish French |
45 | + |
46 | +To remove a language you'll need to re-enter the entire list, removing the language you no longer want |
47 | + |
48 | + juju set python_moinmoin languages="English Spanish" |
49 | + |
50 | +# xapian_serach |
51 | +Enable xapian serach on your wiki (True or False) |
52 | + |
53 | + juju set python_moinmoin xapian_search="True" |
54 | + |
55 | +# port |
56 | +The port moinmoin should listen on. By default it's 8080 |
57 | + |
58 | + juju set python_moinmoin port=80 |
59 | + |
60 | +# use_openid |
61 | +Setting to say if the wiki is using OpenID authentication or not. |
62 | + |
63 | + juju set python_moinmoin use_openid=True |
64 | + |
65 | +# openidrp_authorized_teams |
66 | +What OpenID teams are authorized to log in, if use_openid is true. |
67 | + |
68 | + juju set python_moinmoin openidrp_authorized_teams="['team1', 'team2']" |
69 | + |
70 | +# Relations |
71 | +python-moinmoin can be related to any of the proxy charms, including but not limited to: haproxy, varnish, and squid. An example of connecting squid-reverseproxy to python-moinmoin is listed below: |
72 | + |
73 | + juju deploy squid-reverseproxy squid |
74 | + juju unexpose python-moinmoin |
75 | + juju add-relation python-moinmoin squid |
76 | + juju expose squid |
77 | |
78 | === removed file 'README.md' |
79 | --- README.md 2013-05-23 13:02:52 +0000 |
80 | +++ README.md 1970-01-01 00:00:00 +0000 |
81 | @@ -1,62 +0,0 @@ |
82 | -# Overview |
83 | -Juju charm for python-moinmoin, charm author: Patrick Hetu <patrick@koumbit.org> |
84 | - |
85 | -# Deployment |
86 | -After a successful bootstrap, you can deploy the service without a configuration or alias with: |
87 | - |
88 | - juju deploy python-moinmoin |
89 | - juju expose python-moinmoin |
90 | - |
91 | -Though, it's recommended you create a yaml file with your parameters, for instance if you were to deploy the service as "mywiki", the configuration would look like: |
92 | - |
93 | - mywiki: |
94 | - wiki_name : "My MoinMoin wiki" |
95 | - admin_name: "Admin" |
96 | - languages: English French |
97 | - xapian_search: True |
98 | - port: 80 |
99 | - |
100 | -To then deploy the wiki, with pre-seeded configuration: |
101 | - |
102 | - juju deploy --config mywiki.yaml python-moinmoin mywiki |
103 | - juju expose mywiki |
104 | - |
105 | -# Configuration |
106 | -This service contains several configuration options for controlling the moinmoin install |
107 | - |
108 | -## wiki_name |
109 | -Set the name and title of the wiki |
110 | - |
111 | - juju set python_moinmoin wiki_name="My new wiki name" |
112 | - |
113 | -## admin_name |
114 | -Name of the admin account |
115 | - |
116 | - juju set python_moinmoin admin_name="Adminstrator" |
117 | - |
118 | -## languages |
119 | -List of languages to be installed on the wiki, space delimited |
120 | - |
121 | - juju set python_moinmoin languages English Spanish French |
122 | - |
123 | -To remove a language you'll need to re-enter the entire list, removing the language you no longer want |
124 | - |
125 | - juju set python_moinmoin languages="English Spanish" |
126 | - |
127 | -# xapian_serach |
128 | -Enable xapian serach on your wiki (True or False) |
129 | - |
130 | - juju set python_moinmoin xapian_search="True" |
131 | - |
132 | -# port |
133 | -The port moinmoin should listen on. By default it's 8080 |
134 | - |
135 | - juju set python_moinmoin port=80 |
136 | - |
137 | -# Relations |
138 | -python-moinmoin can be related to any of the proxy charms, including but not limited to: haproxy, varnish, and squid. An example of connecting squid-reverseproxy to python-moinmoin is listed below: |
139 | - |
140 | - juju deploy squid-reverseproxy squid |
141 | - juju unexpose python-moinmoin |
142 | - juju add-relation python-moinmoin squid |
143 | - juju expose squid |
144 | |
145 | === added file 'config.yaml' |
146 | --- config.yaml 1970-01-01 00:00:00 +0000 |
147 | +++ config.yaml 2013-09-26 06:24:47 +0000 |
148 | @@ -0,0 +1,42 @@ |
149 | +options: |
150 | + wiki_name: |
151 | + default: "My MoinMoin wiki" |
152 | + type: string |
153 | + description: The name of your wiki. |
154 | + admin_name: |
155 | + default: "Admin" |
156 | + type: string |
157 | + description: The wiki name of the admin user. |
158 | + languages: |
159 | + default: "English" |
160 | + type: string |
161 | + description: | |
162 | + Languages to installed in a space-separated format. |
163 | + For available languages see: http://moinmo.in/4ct10n/AttachFile/LanguageSetup?action=AttachFile |
164 | + (write only the language name part) |
165 | + loglevel: |
166 | + default: "INFO" |
167 | + type: string |
168 | + description: Default loglevel, to adjust verbosity DEBUG, INFO, WARNING, ERROR, CRITICAL |
169 | + xapian_search: |
170 | + default: "True" |
171 | + type: string |
172 | + description: Set to True to enable the Xapian search engine. |
173 | + listen_port: |
174 | + default: 8080 |
175 | + type: int |
176 | + description: The port for the service to listen on. |
177 | + use_openid: |
178 | + default: "False" |
179 | + type: string |
180 | + description: OpenID authentication used or not. |
181 | + openidrp_authorized_teams: |
182 | + default: "False" |
183 | + type: string |
184 | + description: Define the openid authorized teams for the wiki. |
185 | + extra_settings: |
186 | + default: "" |
187 | + type: string |
188 | + description: | |
189 | + For the list of available configuration options see: http://moinmo.in/HelpOnConfiguration. |
190 | + Also, don't forget to starts your configuration lines with a 4 spaces indentation. |
191 | |
192 | === removed file 'config.yaml' |
193 | --- config.yaml 2013-05-07 16:03:30 +0000 |
194 | +++ config.yaml 1970-01-01 00:00:00 +0000 |
195 | @@ -1,30 +0,0 @@ |
196 | -options: |
197 | - wiki_name: |
198 | - default: "My MoinMoin wiki" |
199 | - type: string |
200 | - description: The name of your wiki. |
201 | - admin_name: |
202 | - default: "Admin" |
203 | - type: string |
204 | - description: The wiki name of the admin user. |
205 | - languages: |
206 | - default: "English" |
207 | - type: string |
208 | - description: | |
209 | - Languages to installed in a space-separated format. |
210 | - For available languages see: http://moinmo.in/4ct10n/AttachFile/LanguageSetup?action=AttachFile |
211 | - (write only the language name part) |
212 | - xapian_search: |
213 | - default: "True" |
214 | - type: string |
215 | - description: Set to True to enable the Xapian search engine. |
216 | - listen_port: |
217 | - default: 8080 |
218 | - type: int |
219 | - description: The port for the service to listen on. |
220 | - extra_settings: |
221 | - default: "" |
222 | - type: string |
223 | - description: | |
224 | - For the list of available configuration options see: http://moinmo.in/HelpOnConfiguration. |
225 | - Also, don't forget to starts your configuration lines with a 4 spaces indentation. |
226 | |
227 | === added file 'copyright' |
228 | --- copyright 1970-01-01 00:00:00 +0000 |
229 | +++ copyright 2013-09-26 06:24:47 +0000 |
230 | @@ -0,0 +1,18 @@ |
231 | +Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ |
232 | + |
233 | +Files: * |
234 | +Copyright: Copyright 2013, Canonical Ltd., All Rights Reserved. |
235 | +License: GPL-3 |
236 | + This program is free software: you can redistribute it and/or modify |
237 | + it under the terms of the GNU General Public License as published by |
238 | + the Free Software Foundation, either version 3 of the License, or |
239 | + (at your option) any later version. |
240 | + . |
241 | + This program is distributed in the hope that it will be useful, |
242 | + but WITHOUT ANY WARRANTY; without even the implied warranty of |
243 | + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
244 | + GNU General Public License for more details. |
245 | + . |
246 | + You should have received a copy of the GNU General Public License |
247 | + along with this program. If not, see <http://www.gnu.org/licenses/>. |
248 | + |
249 | |
250 | === removed file 'copyright' |
251 | --- copyright 2011-11-01 20:33:21 +0000 |
252 | +++ copyright 1970-01-01 00:00:00 +0000 |
253 | @@ -1,17 +0,0 @@ |
254 | -Format: http://dep.debian.net/deps/dep5/ |
255 | - |
256 | -Files: * |
257 | -Copyright: Copyright 2011, Patrick Hetu <patrick@koumbit.org>, All Rights Reserved. |
258 | -License: GPL-3 |
259 | - This program is free software: you can redistribute it and/or modify |
260 | - it under the terms of the GNU General Public License as published by |
261 | - the Free Software Foundation, either version 3 of the License, or |
262 | - (at your option) any later version. |
263 | - . |
264 | - This program is distributed in the hope that it will be useful, |
265 | - but WITHOUT ANY WARRANTY; without even the implied warranty of |
266 | - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
267 | - GNU General Public License for more details. |
268 | - . |
269 | - You should have received a copy of the GNU General Public License |
270 | - along with this program. If not, see <http://www.gnu.org/licenses/>. |
271 | |
272 | === added directory 'files' |
273 | === added file 'files/openidrp.py' |
274 | --- files/openidrp.py 1970-01-01 00:00:00 +0000 |
275 | +++ files/openidrp.py 2013-09-26 06:24:47 +0000 |
276 | @@ -0,0 +1,325 @@ |
277 | +# -*- coding: iso-8859-1 -*- |
278 | +""" |
279 | + MoinMoin - OpenID authorization |
280 | + |
281 | + @copyright: 2007 MoinMoin:JohannesBerg |
282 | + @license: GNU GPL, see COPYING for details. |
283 | +""" |
284 | +from MoinMoin import log |
285 | +logging = log.getLogger(__name__) |
286 | + |
287 | +from MoinMoin.util.moinoid import MoinOpenIDStore |
288 | +from MoinMoin import user |
289 | +from MoinMoin.auth import BaseAuth |
290 | +from openid.consumer import consumer |
291 | +from openid.yadis.discover import DiscoveryFailure |
292 | +from openid.fetchers import HTTPFetchingError |
293 | +from MoinMoin.widget import html |
294 | +from MoinMoin.auth import CancelLogin, ContinueLogin |
295 | +from MoinMoin.auth import MultistageFormLogin, MultistageRedirectLogin |
296 | +from MoinMoin.auth import get_multistage_continuation_url |
297 | +from werkzeug import url_encode |
298 | + |
299 | +class OpenIDAuth(BaseAuth): |
300 | + login_inputs = ['openid_identifier'] |
301 | + name = 'openid' |
302 | + logout_possible = True |
303 | + auth_attribs = () |
304 | + |
305 | + def __init__(self, modify_request=None, |
306 | + update_user=None, |
307 | + create_user=None, |
308 | + forced_service=None, |
309 | + idselector_com=None): |
310 | + BaseAuth.__init__(self) |
311 | + self._modify_request = modify_request or (lambda x, c: None) |
312 | + self._update_user = update_user or (lambda i, u, c: None) |
313 | + self._create_user = create_user or (lambda i, u, c: None) |
314 | + self._forced_service = forced_service |
315 | + self._idselector_com = idselector_com |
316 | + if forced_service: |
317 | + self.login_inputs = ['special_no_input'] |
318 | + |
319 | + def _handle_user_data(self, request, u): |
320 | + create = not u |
321 | + # just in case the wiki admin screwed up |
322 | + if create and user.getUserIdByOpenId(request, request.session['openid.id']): |
323 | + return None |
324 | + |
325 | + if create: |
326 | + # pass in a created but unsaved user object |
327 | + u = user.User(request, auth_method=self.name, |
328 | + auth_username=request.session['openid.id'], |
329 | + auth_attribs=self.auth_attribs) |
330 | + # invalid name |
331 | + u.name = '' |
332 | + u = self._create_user(request.session['openid.info'], u, request.cfg) |
333 | + |
334 | + if u: |
335 | + self._update_user(request.session['openid.info'], u, request.cfg) |
336 | + |
337 | + if not user.isValidName(request, u.name): |
338 | + return None |
339 | + |
340 | + if not hasattr(u, 'openids'): |
341 | + u.openids = [] |
342 | + if not request.session['openid.id'] in u.openids: |
343 | + u.openids.append(request.session['openid.id']) |
344 | + |
345 | + u.save() |
346 | + |
347 | + del request.session['openid.id'] |
348 | + del request.session['openid.info'] |
349 | + |
350 | + return u |
351 | + |
352 | + def _get_account_name(self, request, form, msg=None): |
353 | + # now we need to ask the user for a new username |
354 | + # that they want to use on this wiki |
355 | + # XXX: request nickname from OP and suggest using it |
356 | + # (if it isn't in use yet) |
357 | + logging.debug("running _get_account_name") |
358 | + _ = request.getText |
359 | + form.append(html.INPUT(type='hidden', name='oidstage', value='2')) |
360 | + table = html.TABLE(border='0') |
361 | + form.append(table) |
362 | + td = html.TD(colspan=2) |
363 | + td.append(html.Raw(_("""Please choose an account name now. |
364 | +If you choose an existing account name you will be asked for the |
365 | +password and be able to associate the account with your OpenID."""))) |
366 | + table.append(html.TR().append(td)) |
367 | + if msg: |
368 | + td = html.TD(colspan='2') |
369 | + td.append(html.P().append(html.STRONG().append(html.Raw(msg)))) |
370 | + table.append(html.TR().append(td)) |
371 | + td1 = html.TD() |
372 | + td1.append(html.STRONG().append(html.Raw(_('Name')))) |
373 | + td2 = html.TD() |
374 | + td2.append(html.INPUT(type='text', name='username')) |
375 | + table.append(html.TR().append(td1).append(td2)) |
376 | + td1 = html.TD() |
377 | + td2 = html.TD() |
378 | + td2.append(html.INPUT(type='submit', name='submit', |
379 | + value=_('Choose this name'))) |
380 | + table.append(html.TR().append(td1).append(td2)) |
381 | + |
382 | + def _get_account_name_inval_user(self, request, form): |
383 | + _ = request.getText |
384 | + msg = _('This is not a valid username, choose a different one.') |
385 | + return self._get_account_name(request, form, msg=msg) |
386 | + |
387 | + def _associate_account(self, request, form, accountname, msg=None): |
388 | + _ = request.getText |
389 | + |
390 | + form.append(html.INPUT(type='hidden', name='oidstage', value='3')) |
391 | + table = html.TABLE(border='0') |
392 | + form.append(table) |
393 | + td = html.TD(colspan=2) |
394 | + td.append(html.Raw(_("""The username you have chosen is already |
395 | +taken. If it is your username, enter your password below to associate |
396 | +the username with your OpenID. Otherwise, please choose a different |
397 | +username and leave the password field blank."""))) |
398 | + table.append(html.TR().append(td)) |
399 | + if msg: |
400 | + td.append(html.P().append(html.STRONG().append(html.Raw(msg)))) |
401 | + td1 = html.TD() |
402 | + td1.append(html.STRONG().append(html.Raw(_('Name')))) |
403 | + td2 = html.TD() |
404 | + td2.append(html.INPUT(type='text', name='username', value=accountname)) |
405 | + table.append(html.TR().append(td1).append(td2)) |
406 | + td1 = html.TD() |
407 | + td1.append(html.STRONG().append(html.Raw(_('Password')))) |
408 | + td2 = html.TD() |
409 | + td2.append(html.INPUT(type='password', name='password')) |
410 | + table.append(html.TR().append(td1).append(td2)) |
411 | + td1 = html.TD() |
412 | + td2 = html.TD() |
413 | + td2.append(html.INPUT(type='submit', name='submit', |
414 | + value=_('Associate this name'))) |
415 | + table.append(html.TR().append(td1).append(td2)) |
416 | + |
417 | + def _handle_verify_continuation(self, request): |
418 | + _ = request.getText |
419 | + oidconsumer = consumer.Consumer(request.session, |
420 | + MoinOpenIDStore(request)) |
421 | + query = {} |
422 | + for key in request.values.keys(): |
423 | + query[key] = request.values.get(key) |
424 | + current_url = get_multistage_continuation_url(request, self.name, |
425 | + {'oidstage': '1'}) |
426 | + info = oidconsumer.complete(query, current_url) |
427 | + if info.status == consumer.FAILURE: |
428 | + logging.debug(_("OpenID error: %s.") % info.message) |
429 | + return CancelLogin(_('OpenID error: %s.') % info.message) |
430 | + elif info.status == consumer.CANCEL: |
431 | + logging.debug(_("OpenID verification canceled.")) |
432 | + return CancelLogin(_('Verification canceled.')) |
433 | + elif info.status == consumer.SUCCESS: |
434 | + logging.debug(_("OpenID success. id: %s") % info.identity_url) |
435 | + request.session['openid.id'] = info.identity_url |
436 | + request.session['openid.info'] = info |
437 | + |
438 | + # try to find user object |
439 | + uid = user.getUserIdByOpenId(request, info.identity_url) |
440 | + if uid: |
441 | + u = user.User(request, id=uid, auth_method=self.name, |
442 | + auth_username=info.identity_url, |
443 | + auth_attribs=self.auth_attribs) |
444 | + else: |
445 | + u = None |
446 | + |
447 | + # create or update the user according to the registration data |
448 | + u = self._handle_user_data(request, u) |
449 | + if u: |
450 | + return ContinueLogin(u) |
451 | + |
452 | + # if no user found, then we need to ask for a username, |
453 | + # possibly associating an existing account. |
454 | + logging.debug("OpenID: No user found, prompting for username") |
455 | + #request.session['openid.id'] = info.identity_url |
456 | + return MultistageFormLogin(self._get_account_name) |
457 | + else: |
458 | + logging.debug(_("OpenID failure")) |
459 | + return CancelLogin(_('OpenID failure.')) |
460 | + |
461 | + def _handle_name_continuation(self, request): |
462 | + _ = request.getText |
463 | + if not 'openid.id' in request.session: |
464 | + return CancelLogin(_('No OpenID found in session.')) |
465 | + |
466 | + newname = request.form.get('username', '') |
467 | + if not newname: |
468 | + return MultistageFormLogin(self._get_account_name) |
469 | + if not user.isValidName(request, newname): |
470 | + return MultistageFormLogin(self._get_account_name_inval_user) |
471 | + uid = None |
472 | + if newname: |
473 | + uid = user.getUserId(request, newname) |
474 | + if not uid: |
475 | + # we can create a new user with this name :) |
476 | + u = user.User(request, auth_method=self.name, |
477 | + auth_username=request.session['openid.id'], |
478 | + auth_attribs=self.auth_attribs) |
479 | + u.name = newname |
480 | + u = self._handle_user_data(request, u) |
481 | + return ContinueLogin(u) |
482 | + # requested username already exists. if they know the password, |
483 | + # they can associate that account with the openid. |
484 | + assoc = lambda req, form: self._associate_account(req, form, newname) |
485 | + return MultistageFormLogin(assoc) |
486 | + |
487 | + def _handle_associate_continuation(self, request): |
488 | + if not 'openid.id' in request.session: |
489 | + return CancelLogin(_('No OpenID found in session.')) |
490 | + |
491 | + _ = request.getText |
492 | + username = request.form.get('username', '') |
493 | + password = request.form.get('password', '') |
494 | + if not password: |
495 | + return self._handle_name_continuation(request) |
496 | + u = user.User(request, name=username, password=password, |
497 | + auth_method=self.name, |
498 | + auth_username=request.session['openid.id'], |
499 | + auth_attribs=self.auth_attribs) |
500 | + if u.valid: |
501 | + self._handle_user_data(request, u) |
502 | + return ContinueLogin(u, _('Your account is now associated to your OpenID.')) |
503 | + else: |
504 | + msg = _('The password you entered is not valid.') |
505 | + assoc = lambda req, form: self._associate_account(req, form, username, msg=msg) |
506 | + return MultistageFormLogin(assoc) |
507 | + |
508 | + def _handle_continuation(self, request): |
509 | + _ = request.getText |
510 | + oidstage = request.values.get('oidstage') |
511 | + if oidstage == '1': |
512 | + logging.debug('OpenID: handle verify continuation') |
513 | + return self._handle_verify_continuation(request) |
514 | + elif oidstage == '2': |
515 | + logging.debug('OpenID: handle name continuation') |
516 | + return self._handle_name_continuation(request) |
517 | + elif oidstage == '3': |
518 | + logging.debug('OpenID: handle associate continuation') |
519 | + return self._handle_associate_continuation(request) |
520 | + logging.debug('OpenID error: unknown continuation stage') |
521 | + return CancelLogin(_('OpenID error: unknown continuation stage')) |
522 | + |
523 | + def _openid_form(self, request, form, oidhtml): |
524 | + _ = request.getText |
525 | + txt = _('OpenID verification requires that you click this button:') |
526 | + # create JS to automatically submit the form if possible |
527 | + submitjs = """<script type="text/javascript"> |
528 | +<!--// |
529 | +document.getElementById("openid_message").submit(); |
530 | +//--> |
531 | +</script> |
532 | +""" |
533 | + return ''.join([txt, oidhtml, submitjs]) |
534 | + |
535 | + def login(self, request, user_obj, **kw): |
536 | + continuation = kw.get('multistage') |
537 | + |
538 | + if continuation: |
539 | + return self._handle_continuation(request) |
540 | + |
541 | + # openid is designed to work together with other auths |
542 | + if user_obj and user_obj.valid: |
543 | + return ContinueLogin(user_obj) |
544 | + |
545 | + openid_id = kw.get('openid_identifier') |
546 | + |
547 | + # nothing entered? continue... |
548 | + if not self._forced_service and not openid_id: |
549 | + return ContinueLogin(user_obj) |
550 | + |
551 | + _ = request.getText |
552 | + |
553 | + # user entered something but the session can't be stored |
554 | + if not request.cfg.cookie_lifetime[0]: |
555 | + return ContinueLogin(user_obj, |
556 | + _('Anonymous sessions need to be enabled for OpenID login.')) |
557 | + |
558 | + oidconsumer = consumer.Consumer(request.session, |
559 | + MoinOpenIDStore(request)) |
560 | + |
561 | + try: |
562 | + fserv = self._forced_service |
563 | + if fserv: |
564 | + if isinstance(fserv, str) or isinstance(fserv, unicode): |
565 | + oidreq = oidconsumer.begin(fserv) |
566 | + else: |
567 | + oidreq = oidconsumer.beginWithoutDiscovery(fserv) |
568 | + else: |
569 | + oidreq = oidconsumer.begin(openid_id) |
570 | + except HTTPFetchingError: |
571 | + return ContinueLogin(None, _('Failed to resolve OpenID.')) |
572 | + except DiscoveryFailure: |
573 | + return ContinueLogin(None, _('OpenID discovery failure, not a valid OpenID.')) |
574 | + else: |
575 | + if oidreq is None: |
576 | + return ContinueLogin(None, _('No OpenID.')) |
577 | + |
578 | + self._modify_request(oidreq, request.cfg) |
579 | + |
580 | + return_to = get_multistage_continuation_url(request, self.name, |
581 | + {'oidstage': '1'}) |
582 | + trust_root = request.url_root |
583 | + if oidreq.shouldSendRedirect(): |
584 | + redirect_url = oidreq.redirectURL(trust_root, return_to) |
585 | + return MultistageRedirectLogin(redirect_url) |
586 | + else: |
587 | + form_html = oidreq.formMarkup(trust_root, return_to, |
588 | + form_tag_attrs={'id': 'openid_message'}) |
589 | + mcall = lambda request, form:\ |
590 | + self._openid_form(request, form, form_html) |
591 | + ret = MultistageFormLogin(mcall) |
592 | + return ret |
593 | + |
594 | + def login_hint(self, request): |
595 | + _ = request.getText |
596 | + msg = u'' |
597 | + if self._idselector_com: |
598 | + msg = self._idselector_com |
599 | + msg += _("If you do not have an account yet, you can still log in " |
600 | + "with your OpenID and create one during login.") |
601 | + return msg |
602 | |
603 | === added file 'files/openidrp_teams.py' |
604 | --- files/openidrp_teams.py 1970-01-01 00:00:00 +0000 |
605 | +++ files/openidrp_teams.py 2013-09-26 06:24:47 +0000 |
606 | @@ -0,0 +1,157 @@ |
607 | +# -*- coding: iso-8859-1 -*- |
608 | +""" |
609 | + MoinMoin - Launchpad Teams Extension for OpenID authorization |
610 | + |
611 | + @copyright: 2009 Canonical, Inc. |
612 | + @license: GNU GPL, see COPYING for details. |
613 | +""" |
614 | +import time |
615 | +import re |
616 | +import os |
617 | +import logging |
618 | +import fcntl |
619 | +import errno |
620 | +import copy |
621 | + |
622 | +#from MoinMoin.util.moinoid import MoinOpenIDStore |
623 | +from MoinMoin import user |
624 | +from MoinMoin.auth import BaseAuth |
625 | +from MoinMoin.auth.openidrp import OpenIDAuth |
626 | +#OpenIDSREGAuth |
627 | +#from openid.consumer import consumer |
628 | +#from openid.yadis.discover import DiscoveryFailure |
629 | +#from openid.fetchers import HTTPFetchingError |
630 | +#from MoinMoin.widget import html |
631 | +#from MoinMoin.auth import CancelLogin, ContinueLogin |
632 | +#from MoinMoin.auth import MultistageFormLogin, MultistageRedirectLogin |
633 | +#from MoinMoin.auth import get_multistage_continuation_url |
634 | + |
635 | +from .teams import TeamsRequest, TeamsResponse, supportsTeams |
636 | +from MoinMoin import wikiutil |
637 | +from MoinMoin.PageEditor import PageEditor, conflict_markers |
638 | +from MoinMoin.Page import Page |
639 | + |
640 | +def openidrp_teams_modify_request(oidreq, cfg): |
641 | + # Request Launchpad teams information, if configured |
642 | + # should also check supportsTeams() result |
643 | + #if teams_extension_avail and len(cfg.openidrp_authorized_teams) > 0: |
644 | + if len(cfg.openidrp_authorized_teams) > 0: |
645 | + oidreq.addExtension(TeamsRequest(cfg.openidrp_authorized_teams)) |
646 | + |
647 | +def openidrp_teams_create_user(info, u, cfg): |
648 | + # Check for Launchpad teams data in response |
649 | + teams = None |
650 | + #if teams_extension_avail and len(cfg.openidrp_authorized_teams) > 0: |
651 | + teams_response = TeamsResponse.fromSuccessResponse(info) |
652 | + teams = teams_response.is_member |
653 | + if teams: |
654 | + _save_teams_acl(u, teams, cfg) |
655 | + return u |
656 | + |
657 | +def openidrp_teams_update_user(info, u, cfg): |
658 | + teams = None |
659 | + teams_response = TeamsResponse.fromSuccessResponse(info) |
660 | + teams = teams_response.is_member |
661 | + if teams: |
662 | + _save_teams_acl(u, teams, cfg) |
663 | + |
664 | +# Take a list of Launchpad teams and add the user to the ACL pages |
665 | +# ACL group names cannot have "-" in them, although team names do. |
666 | +def _save_teams_acl(u, teams, cfg): |
667 | + logging.log(logging.INFO, "running save_teams_acl...") |
668 | + |
669 | + # remove any teams the user is no longer in |
670 | + if not hasattr(u, 'teams'): |
671 | + u.teams = [] |
672 | + logging.log(logging.INFO, "old teams: " + str(u.teams) |
673 | + + " new teams: " + str(teams)) |
674 | + |
675 | + for t in u.teams: |
676 | + if not t in teams: |
677 | + logging.log(logging.INFO, "remove user from team: " + t) |
678 | + team = t.strip().replace("-", "") |
679 | + _remove_user_from_team(u, team, cfg) |
680 | + |
681 | + for t in teams: |
682 | + team = t.strip().replace("-", "") |
683 | + if not team: |
684 | + continue |
685 | + logging.log(logging.INFO, "Launchpad team: " + team) |
686 | + _add_user_to_team(u, team, cfg) |
687 | + |
688 | + u.teams = teams |
689 | + u.save() |
690 | + |
691 | +def _add_user_to_team(u, team, cfg): |
692 | + # use admin account to create or edit ACL page |
693 | + # http://moinmo.in/MoinDev/CommonTasks |
694 | + acl_request = u._request |
695 | + acl_request.user = user.User(acl_request, None, cfg.openidrp_acl_admin) |
696 | + |
697 | + # PageEditor() is not safe to use concurrently - and browsers |
698 | + # offten try to relogin to multiple tabs on the same wiki |
699 | + # concurrently - due to its underlying reliance on the 'current' |
700 | + # file to determine the current revision and the fact that |
701 | + # PageEditor.saveText() moves 'current' out of the way as a |
702 | + # lockfile. If you lose the race, you end up getting an |
703 | + # apparently empty body for a page which in fact has content. |
704 | + # |
705 | + # Work around this by doing our own locking. |
706 | + |
707 | + pagedir = os.path.join(u._request.cfg.data_dir, "pages", |
708 | + team + cfg.openidrp_acl_page_postfix) |
709 | + if not os.path.exists(pagedir): |
710 | + os.mkdir(pagedir) |
711 | + lockfile = os.path.join(pagedir, "openid.lock") |
712 | + lock_fd = os.open(lockfile, os.O_RDWR | os.O_CREAT) |
713 | + got_lock = False |
714 | + retry = 0 |
715 | + while not got_lock and retry < 25: |
716 | + retry += 1 |
717 | + try: |
718 | + fcntl.lockf(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB) |
719 | + got_lock = True |
720 | + except IOError, e: |
721 | + got_lock = False |
722 | + if (e.errno == errno.EACCES or e.errno == errno.EAGAIN): |
723 | + time.sleep(0.1) |
724 | + else: |
725 | + raise |
726 | + # If we can't get a lock, give up and return quietly. The user |
727 | + # will simply have to logout and back in. |
728 | + if not got_lock: |
729 | + logging.log(logging.WARNING, "Could not acquire lock on " + lockfile) |
730 | + return |
731 | + |
732 | + pe = PageEditor(acl_request, team + cfg.openidrp_acl_page_postfix) |
733 | + acl_text = pe.get_raw_body() |
734 | + logging.log(logging.INFO, "ACL Page content: " + acl_text) |
735 | + # make sure acl command is first line of document |
736 | + # only the admin user specified in wikiconfig should |
737 | + # be allowed to change these acl files |
738 | + if not acl_text or acl_text == "" or acl_text[0] != "#": |
739 | + acl_text = "#acl Known:read All:\n" + acl_text |
740 | + # does ACL want uid, name, username, auth_username? |
741 | + p = re.compile(ur"^ \* %s$" % u.name, re.MULTILINE) |
742 | + if not p.search(acl_text): |
743 | + logging.log(logging.INFO, "did not find user %s in acl, adding..." % u.name) |
744 | + acl_text += u" * %s\n" % u.name |
745 | + pe.saveText(acl_text, 0) |
746 | + os.close(lock_fd) |
747 | + |
748 | +def _remove_user_from_team(u, team, cfg): |
749 | + acl_request = u._request |
750 | + acl_request.user = user.User(acl_request, None, cfg.openidrp_acl_admin) |
751 | + pe = PageEditor(acl_request, team + cfg.openidrp_acl_page_postfix) |
752 | + acl_text = pe.get_raw_body() |
753 | + logging.log(logging.INFO, "ACL Page content: " + acl_text) |
754 | + # does ACL want uid, name, username, auth_username? |
755 | + p = re.compile(ur"^ \* %s$" % u.name, re.MULTILINE) |
756 | + if p.search(acl_text): |
757 | + logging.log(logging.INFO, "found user %s in acl, removing..." % u.name) |
758 | + acl_text = acl_text.replace(" * %s\n" % u.name, "") |
759 | + try: |
760 | + pe.saveText(acl_text, 0) |
761 | + except PageEditor.EmptyPage: |
762 | + pe.deletePage() |
763 | + |
764 | |
765 | === added file 'files/teams.py' |
766 | --- files/teams.py 1970-01-01 00:00:00 +0000 |
767 | +++ files/teams.py 2013-09-26 06:24:47 +0000 |
768 | @@ -0,0 +1,398 @@ |
769 | +# Copyright (C) 2009, 2010 Canonical Ltd |
770 | +# |
771 | +# This program is free software: you can redistribute it and/or modify |
772 | +# it under the terms of the GNU General Public License as published by |
773 | +# the Free Software Foundation, either version 3 of the License, or |
774 | +# (at your option) any later version. |
775 | +# |
776 | +# This program is distributed in the hope that it will be useful, |
777 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
778 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
779 | +# GNU General Public License for more details. |
780 | +# |
781 | +# You should have received a copy of the GNU General Public License |
782 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
783 | + |
784 | +"""Team membership support for Launchpad. |
785 | + |
786 | +The primary form of communication between the RP and Launchpad is an |
787 | +OpenID authentication request. Our solution is to piggyback a team |
788 | +membership test onto this interaction. |
789 | + |
790 | +As part of an OpenID authentication request, the RP includes the |
791 | +following fields: |
792 | + |
793 | + openid.ns.lp: |
794 | + An OpenID 2.0 namespace URI for the extension. It is not strictly |
795 | + required for 1.1 requests, but including it is good for forward |
796 | + compatibility. |
797 | + |
798 | + It must be set to: http://ns.launchpad.net/2007/openid-teams |
799 | + |
800 | + openid.lp.query_membership: |
801 | + A comma separated list of Launchpad team names that the RP is |
802 | + interested in. |
803 | + |
804 | +As part of the positive assertion OpenID response, the following field |
805 | +will be provided: |
806 | + |
807 | + openid.ns.lp: |
808 | + (as above) |
809 | + |
810 | + openid.lp.is_member: |
811 | + A comma separated list of teams that the user is actually a member |
812 | + of. The list may be limited to those teams mentioned in the |
813 | + request. |
814 | + |
815 | + This field must be included in the response signature in order to |
816 | + be considered valid (as the response is bounced through the user's |
817 | + web browser, an unsigned value could be modified). |
818 | + |
819 | +@since: 2.1.1 |
820 | +""" |
821 | + |
822 | +from openid.message import registerNamespaceAlias, \ |
823 | + NamespaceAliasRegistrationError |
824 | +from openid.extension import Extension |
825 | +from openid import oidutil |
826 | + |
827 | +try: |
828 | + basestring #pylint:disable-msg=W0104 |
829 | +except NameError: |
830 | + # For Python 2.2 |
831 | + basestring = (str, unicode) #pylint:disable-msg=W0622 |
832 | + |
833 | +__all__ = [ |
834 | + 'TeamsRequest', |
835 | + 'TeamsResponse', |
836 | + 'ns_uri', |
837 | + 'supportsTeams', |
838 | + ] |
839 | + |
840 | +ns_uri = 'http://ns.launchpad.net/2007/openid-teams' |
841 | + |
842 | +try: |
843 | + registerNamespaceAlias(ns_uri, 'lp') |
844 | +except NamespaceAliasRegistrationError, e: |
845 | + oidutil.log('registerNamespaceAlias(%r, %r) failed: %s' % (ns_uri, |
846 | + 'lp', str(e),)) |
847 | + |
848 | +def supportsTeams(endpoint): |
849 | + """Does the given endpoint advertise support for Launchpad Teams? |
850 | + |
851 | + @param endpoint: The endpoint object as returned by OpenID discovery |
852 | + @type endpoint: openid.consumer.discover.OpenIDEndpoint |
853 | + |
854 | + @returns: Whether an lp type was advertised by the endpoint |
855 | + @rtype: bool |
856 | + """ |
857 | + return endpoint.usesExtension(ns_uri) |
858 | + |
859 | +class TeamsNamespaceError(ValueError): |
860 | + """The Launchpad teams namespace was not found and could not |
861 | + be created using the expected name (there's another extension |
862 | + using the name 'lp') |
863 | + |
864 | + This is not I{illegal}, for OpenID 2, although it probably |
865 | + indicates a problem, since it's not expected that other extensions |
866 | + will re-use the alias that is in use for OpenID 1. |
867 | + |
868 | + If this is an OpenID 1 request, then there is no recourse. This |
869 | + should not happen unless some code has modified the namespaces for |
870 | + the message that is being processed. |
871 | + """ |
872 | + |
873 | +def getTeamsNS(message): |
874 | + """Extract the Launchpad teams namespace URI from the given |
875 | + OpenID message. |
876 | + |
877 | + @param message: The OpenID message from which to parse Launchpad |
878 | + teams. This may be a request or response message. |
879 | + @type message: C{L{openid.message.Message}} |
880 | + |
881 | + @returns: the lp namespace URI for the supplied message. The |
882 | + message may be modified to define a Launchpad teams |
883 | + namespace. |
884 | + @rtype: C{str} |
885 | + |
886 | + @raise ValueError: when using OpenID 1 if the message defines |
887 | + the 'lp' alias to be something other than a Launchpad |
888 | + teams type. |
889 | + """ |
890 | + # See if there exists an alias for the Launchpad teams type. |
891 | + alias = message.namespaces.getAlias(ns_uri) |
892 | + if alias is None: |
893 | + # There is no alias, so try to add one. (OpenID version 1) |
894 | + try: |
895 | + message.namespaces.addAlias(ns_uri, 'lp') |
896 | + except KeyError, why: |
897 | + # An alias for the string 'lp' already exists, but it's |
898 | + # defined for something other than Launchpad teams |
899 | + raise TeamsNamespaceError(why[0]) |
900 | + |
901 | + # we know that ns_uri defined, because it's defined in the |
902 | + # else clause of the loop as well, so disable the warning |
903 | + return ns_uri #pylint:disable-msg=W0631 |
904 | + |
905 | +class TeamsRequest(Extension): |
906 | + """An object to hold the state of a Launchpad teams request. |
907 | + |
908 | + @ivar query_membership: A comma separated list of Launchpad team |
909 | + names that the RP is interested in. |
910 | + @type required: [str] |
911 | + |
912 | + @group Consumer: requestField, requestTeams, getExtensionArgs, addToOpenIDRequest |
913 | + @group Server: fromOpenIDRequest, parseExtensionArgs |
914 | + """ |
915 | + |
916 | + ns_alias = 'lp' |
917 | + |
918 | + def __init__(self, query_membership=None, lp_ns_uri=ns_uri): |
919 | + """Initialize an empty Launchpad teams request""" |
920 | + Extension.__init__(self) |
921 | + self.query_membership = [] |
922 | + self.ns_uri = lp_ns_uri |
923 | + |
924 | + if query_membership: |
925 | + self.requestTeams(query_membership) |
926 | + |
927 | + # Assign getTeamsNS to a static method so that it can be |
928 | + # overridden for testing. |
929 | + _getTeamsNS = staticmethod(getTeamsNS) |
930 | + |
931 | + def fromOpenIDRequest(cls, request): |
932 | + """Create a Launchpad teams request that contains the |
933 | + fields that were requested in the OpenID request with the |
934 | + given arguments |
935 | + |
936 | + @param request: The OpenID request |
937 | + @type request: openid.server.CheckIDRequest |
938 | + |
939 | + @returns: The newly created Launchpad teams request |
940 | + @rtype: C{L{TeamsRequest}} |
941 | + """ |
942 | + self = cls() |
943 | + |
944 | + # Since we're going to mess with namespace URI mapping, don't |
945 | + # mutate the object that was passed in. |
946 | + message = request.message.copy() |
947 | + |
948 | + self.ns_uri = self._getTeamsNS(message) |
949 | + args = message.getArgs(self.ns_uri) |
950 | + self.parseExtensionArgs(args) |
951 | + |
952 | + return self |
953 | + |
954 | + fromOpenIDRequest = classmethod(fromOpenIDRequest) |
955 | + |
956 | + def parseExtensionArgs(self, args, strict=False): |
957 | + """Parse the unqualified Launchpad teams request |
958 | + parameters and add them to this object. |
959 | + |
960 | + This method is essentially the inverse of |
961 | + C{L{getExtensionArgs}}. This method restores the serialized |
962 | + Launchpad teams request fields. |
963 | + |
964 | + If you are extracting arguments from a standard OpenID |
965 | + checkid_* request, you probably want to use C{L{fromOpenIDRequest}}, |
966 | + which will extract the lp namespace and arguments from the |
967 | + OpenID request. This method is intended for cases where the |
968 | + OpenID server needs more control over how the arguments are |
969 | + parsed than that method provides. |
970 | + |
971 | + >>> args = message.getArgs(ns_uri) |
972 | + >>> request.parseExtensionArgs(args) |
973 | + |
974 | + @param args: The unqualified Launchpad teams arguments |
975 | + @type args: {str:str} |
976 | + |
977 | + @param strict: Whether requests with fields that are not |
978 | + defined in the Launchpad teams specification should be |
979 | + tolerated (and ignored) |
980 | + @type strict: bool |
981 | + |
982 | + @returns: None; updates this object |
983 | + """ |
984 | + items = args.get('query_membership') |
985 | + if items: |
986 | + for team_name in items.split(','): |
987 | + try: |
988 | + self.requestTeam(team_name, strict) |
989 | + except ValueError: |
990 | + if strict: |
991 | + raise |
992 | + |
993 | + def allRequestedTeams(self): |
994 | + """A list of all of the Launchpad teams that were |
995 | + requested. |
996 | + |
997 | + @rtype: [str] |
998 | + """ |
999 | + return self.query_membership |
1000 | + |
1001 | + def wereTeamsRequested(self): |
1002 | + """Have any Launchpad teams been requested? |
1003 | + |
1004 | + @rtype: bool |
1005 | + """ |
1006 | + return bool(self.allRequestedTeams()) |
1007 | + |
1008 | + def __contains__(self, team_name): |
1009 | + """Was this team in the request?""" |
1010 | + return team_name in self.query_membership |
1011 | + |
1012 | + def requestTeam(self, team_name, strict=False): |
1013 | + """Request the specified team from the OpenID user |
1014 | + |
1015 | + @param team_name: the unqualified Launchpad team name |
1016 | + @type team_name: str |
1017 | + |
1018 | + @param strict: whether to raise an exception when a team is |
1019 | + added to a request more than once |
1020 | + |
1021 | + @raise ValueError: when strict is set and the team was |
1022 | + requested more than once |
1023 | + """ |
1024 | + if strict: |
1025 | + if team_name in self.query_membership: |
1026 | + raise ValueError('That team has already been requested') |
1027 | + else: |
1028 | + if team_name in self.query_membership: |
1029 | + return |
1030 | + |
1031 | + self.query_membership.append(team_name) |
1032 | + |
1033 | + def requestTeams(self, query_membership, strict=False): |
1034 | + """Add the given list of teams to the request |
1035 | + |
1036 | + @param query_membership: The Launchpad teams request |
1037 | + @type query_membership: [str] |
1038 | + |
1039 | + @raise ValueError: when a team requested is not a string |
1040 | + or strict is set and a team was requested more than once |
1041 | + """ |
1042 | + if isinstance(query_membership, basestring): |
1043 | + raise TypeError('Teams should be passed as a list of ' |
1044 | + 'strings (not %r)' % (type(query_membership),)) |
1045 | + |
1046 | + for team_name in query_membership: |
1047 | + self.requestTeam(team_name, strict=strict) |
1048 | + |
1049 | + def getExtensionArgs(self): |
1050 | + """Get a dictionary of unqualified Launchpad teams |
1051 | + arguments representing this request. |
1052 | + |
1053 | + This method is essentially the inverse of |
1054 | + C{L{parseExtensionArgs}}. This method serializes the Launchpad |
1055 | + teams request fields. |
1056 | + |
1057 | + @rtype: {str:str} |
1058 | + """ |
1059 | + args = {} |
1060 | + |
1061 | + if self.query_membership: |
1062 | + args['query_membership'] = ','.join(self.query_membership) |
1063 | + |
1064 | + return args |
1065 | + |
1066 | +class TeamsResponse(Extension): |
1067 | + """Represents the data returned in a Launchpad teams response |
1068 | + inside of an OpenID C{id_res} response. This object will be |
1069 | + created by the OpenID server, added to the C{id_res} response |
1070 | + object, and then extracted from the C{id_res} message by the |
1071 | + Consumer. |
1072 | + |
1073 | + @ivar data: The Launchpad teams data, an array. |
1074 | + |
1075 | + @ivar ns_uri: The URI under which the Launchpad teams data was |
1076 | + stored in the response message. |
1077 | + |
1078 | + @group Server: extractResponse |
1079 | + @group Consumer: fromSuccessResponse |
1080 | + @group Read-only dictionary interface: keys, iterkeys, items, iteritems, |
1081 | + __iter__, get, __getitem__, keys, has_key |
1082 | + """ |
1083 | + |
1084 | + ns_alias = 'lp' |
1085 | + |
1086 | + def __init__(self, is_member=None, lp_ns_uri=ns_uri): |
1087 | + Extension.__init__(self) |
1088 | + if is_member is None: |
1089 | + self.is_member = [] |
1090 | + else: |
1091 | + self.is_member = is_member |
1092 | + |
1093 | + self.ns_uri = lp_ns_uri |
1094 | + |
1095 | + def addTeam(self, team_name): |
1096 | + if team_name not in self.is_member: |
1097 | + self.is_member.append(team_name) |
1098 | + |
1099 | + def extractResponse(cls, request, is_member_str): |
1100 | + """Take a C{L{TeamsRequest}} and a list of Launchpad |
1101 | + team values and create a C{L{TeamsResponse}} |
1102 | + object containing that data. |
1103 | + |
1104 | + @param request: The Launchpad teams request object |
1105 | + @type request: TeamsRequest |
1106 | + |
1107 | + @param is_member: The Launchpad teams data for this |
1108 | + response, as a list of strings. |
1109 | + @type is_member: {str:str} |
1110 | + |
1111 | + @returns: a Launchpad teams response object |
1112 | + @rtype: TeamsResponse |
1113 | + """ |
1114 | + self = cls() |
1115 | + self.ns_uri = request.ns_uri |
1116 | + self.is_member = is_member_str.split(',') |
1117 | + return self |
1118 | + |
1119 | + extractResponse = classmethod(extractResponse) |
1120 | + |
1121 | + # Assign getTeamsNS to a static method so that it can be |
1122 | + # overridden for testing |
1123 | + _getTeamsNS = staticmethod(getTeamsNS) |
1124 | + |
1125 | + def fromSuccessResponse(cls, success_response, signed_only=True): |
1126 | + """Create a C{L{TeamsResponse}} object from a successful OpenID |
1127 | + library response |
1128 | + (C{L{openid.consumer.consumer.SuccessResponse}}) response |
1129 | + message |
1130 | + |
1131 | + @param success_response: A SuccessResponse from consumer.complete() |
1132 | + @type success_response: C{L{openid.consumer.consumer.SuccessResponse}} |
1133 | + |
1134 | + @param signed_only: Whether to process only data that was |
1135 | + signed in the id_res message from the server. |
1136 | + @type signed_only: bool |
1137 | + |
1138 | + @rtype: TeamsResponse |
1139 | + @returns: A Launchpad teams response containing the data |
1140 | + that was supplied with the C{id_res} response. |
1141 | + """ |
1142 | + self = cls() |
1143 | + self.ns_uri = self._getTeamsNS(success_response.message) |
1144 | + if signed_only: |
1145 | + args = success_response.getSignedNS(self.ns_uri) |
1146 | + else: |
1147 | + args = success_response.message.getArgs(self.ns_uri) |
1148 | + |
1149 | + if "is_member" in args: |
1150 | + is_member_str = args["is_member"] |
1151 | + self.is_member = is_member_str.split(',') |
1152 | + #self.is_member = args["is_member"] |
1153 | + |
1154 | + return self |
1155 | + |
1156 | + fromSuccessResponse = classmethod(fromSuccessResponse) |
1157 | + |
1158 | + def getExtensionArgs(self): |
1159 | + """Get the fields to put in the Launchpad teams namespace |
1160 | + when adding them to an id_res message. |
1161 | + |
1162 | + @see: openid.extension |
1163 | + """ |
1164 | + ns_args = {'is_member': ','.join(self.is_member),} |
1165 | + return ns_args |
1166 | + |
1167 | |
1168 | === added directory 'hooks' |
1169 | === removed directory 'hooks' |
1170 | === added symlink 'hooks/config-changed' |
1171 | === target is u'hooks.py' |
1172 | === removed file 'hooks/config-changed' |
1173 | --- hooks/config-changed 2013-05-07 21:32:01 +0000 |
1174 | +++ hooks/config-changed 1970-01-01 00:00:00 +0000 |
1175 | @@ -1,54 +0,0 @@ |
1176 | -#!/bin/bash |
1177 | - |
1178 | -set -eux |
1179 | - |
1180 | -export UNIT_NAME=`echo $JUJU_UNIT_NAME | cut -d/ -f1` |
1181 | - |
1182 | -export WIKI_NAME=$(config-get wiki_name) |
1183 | -export ADMIN_NAME=$(config-get admin_name) |
1184 | -export LANGUAGES=$(config-get languages) |
1185 | -export XAPIAN_SETTINGS=$(config-get xapian_search) |
1186 | -export EXTRA_SETTINGS=$(config-get extra_settings) |
1187 | -export PORT=$(config-get listen_port) |
1188 | - |
1189 | -juju-log "variables: WIKI_NAME: $WIKI_NAME,ADMIN_NAME: $ADMIN_NAME ,LANGUAGES: $LANGUAGES ,XAPIAN_SETTINGS: $XAPIAN_SETTINGS" |
1190 | - |
1191 | -if [ -f /etc/gunicorn.d/${UNIT_NAME}.conf ]; then |
1192 | - CURRENT_PORT=$(grep '\-\-bind' /etc/gunicorn.d/${UNIT_NAME}.conf | cut -d: -f2 | sed "s/'.*//") |
1193 | -else |
1194 | - CURRENT_PORT=$(config-get listen_port) |
1195 | -fi |
1196 | - |
1197 | -cheetah fill --env -p templates/wikiconfig.py.tmpl > /srv/${UNIT_NAME}/wikiconfig.py |
1198 | - |
1199 | -cheetah fill --env -p templates/wsgi.py.tmpl > /srv/${UNIT_NAME}/wsgi.py |
1200 | - |
1201 | -cheetah fill --env -p templates/logging.conf.tmpl > /srv/${UNIT_NAME}/logging.conf |
1202 | - |
1203 | -cheetah fill --env -p templates/gunicorn.conf.tmpl > /etc/gunicorn.d/${UNIT_NAME}.conf |
1204 | - |
1205 | -if [ $CURRENT_PORT != $PORT ]; then |
1206 | - close-port $CURRENT_PORT/tcp |
1207 | - open-port $PORT/tcp |
1208 | -fi |
1209 | - |
1210 | -if [ "$XAPIAN_SETTINGS" == "True" ] ; then |
1211 | - if [ ! -e /srv/${UNIT_NAME}/xapian_is_initialized ] ; then |
1212 | - moin --config-dir=/srv/${UNIT_NAME}/ index build --mode=add |
1213 | - touch /srv/${UNIT_NAME}/xapian_is_initialized |
1214 | - else |
1215 | - moin --config-dir=/srv/${UNIT_NAME}/ index build --mode=rebuild |
1216 | - fi |
1217 | -fi |
1218 | - |
1219 | -cd /srv/${UNIT_NAME}/ |
1220 | - |
1221 | -for lang in $LANGUAGES; do |
1222 | - python -m MoinMoin.packages i ./underlay/pages/LanguageSetup/attachments/${lang}--all_pages.zip |
1223 | -done |
1224 | - |
1225 | -chown root:www-data /srv/${UNIT_NAME}/ -R |
1226 | -chmod g+rw /srv/${UNIT_NAME}/ -R |
1227 | - |
1228 | -/etc/init.d/gunicorn start |
1229 | -/etc/init.d/gunicorn restart |
1230 | |
1231 | === added file 'hooks/hooks.py' |
1232 | --- hooks/hooks.py 1970-01-01 00:00:00 +0000 |
1233 | +++ hooks/hooks.py 2013-09-26 06:24:47 +0000 |
1234 | @@ -0,0 +1,226 @@ |
1235 | +#!/usr/bin/env python |
1236 | + |
1237 | +# Copyright 2013 Canonical Ltd. All rights reserved. |
1238 | +# Author: Brad Marshall <brad.marshall@canonical.com> |
1239 | + |
1240 | +import glob |
1241 | +import os |
1242 | +import sys |
1243 | +import shutil |
1244 | +import subprocess |
1245 | +import socket |
1246 | +import stat |
1247 | +import pwd |
1248 | +import grp |
1249 | +import json |
1250 | +import yaml |
1251 | +import distutils.sysconfig |
1252 | + |
1253 | +local_copy = os.path.join( |
1254 | + os.path.dirname(os.path.abspath(os.path.dirname(__file__))), |
1255 | + "lib", "charm-helpers") |
1256 | +if os.path.exists(local_copy) and os.path.isdir(local_copy): |
1257 | + sys.path.insert(0, local_copy) |
1258 | + |
1259 | + |
1260 | +from charmhelpers.core.host import ( |
1261 | + mkdir, |
1262 | + service, |
1263 | + service_start, |
1264 | + service_stop, |
1265 | + service_restart, |
1266 | + write_file, |
1267 | +) |
1268 | + |
1269 | +from charmhelpers.core.hookenv import ( |
1270 | + Hooks, |
1271 | + config, |
1272 | + open_port, |
1273 | + close_port, |
1274 | + relation_get, |
1275 | + relation_set, |
1276 | + relation_id, |
1277 | + remote_unit, |
1278 | + local_unit, |
1279 | +) |
1280 | + |
1281 | +from charmhelpers.fetch import ( |
1282 | + apt_install, |
1283 | +) |
1284 | + |
1285 | +unit_name = local_unit().split("/")[0] |
1286 | +hostname = socket.getfqdn() |
1287 | +basedir = "/srv/" + unit_name |
1288 | +rundir = basedir + "/run" |
1289 | +logdir = basedir + "/logs" |
1290 | +staticdir = basedir + "/moin_static" |
1291 | +wikiconfig = basedir + "/wikiconfig.py" |
1292 | +wsgipy = basedir + "/wsgi.py" |
1293 | +loggingconf = basedir + "/logging.conf" |
1294 | +xapian_initialised = basedir + "/xapian_is_initialized" |
1295 | +gunicornconf = "/etc/gunicorn.d/" + unit_name + ".conf" |
1296 | +wiki_name = config().get('wiki_name') |
1297 | +admin_name = config().get('admin_name') |
1298 | +languages = config().get('languages') |
1299 | +loglevel = config().get('loglevel') |
1300 | +xapian_search = config().get('xapian_search') |
1301 | +listen_port = config().get('listen_port') |
1302 | +use_openid = config().get('use_openid') |
1303 | +openidrp_authorized_teams = config().get('openidrp_authorized_teams') |
1304 | +extra_settings = config().get('extra_settings') |
1305 | + |
1306 | +hook_dir = os.path.abspath(os.path.dirname(__file__)) |
1307 | +charm_dir = os.path.dirname(hook_dir) |
1308 | + |
1309 | +required_pkgs = [ |
1310 | + 'python-moinmoin', |
1311 | + 'python-flup', |
1312 | + 'gunicorn', |
1313 | + 'python-eventlet', |
1314 | + 'python-tz', |
1315 | + 'unzip', |
1316 | + 'python-openid', |
1317 | + 'python-xapian', |
1318 | + 'python-xappy', |
1319 | + 'python-docutils', |
1320 | + 'python-jinja2' |
1321 | +] |
1322 | + |
1323 | +hooks = Hooks() |
1324 | + |
1325 | + |
1326 | +@hooks.hook() |
1327 | +def install(): |
1328 | + apt_install(required_pkgs, options=['--force-yes']) |
1329 | + if os.path.exists('/etc/moin/farmconfig.py'): |
1330 | + os.remove('/etc/moin/farmconfig.py') |
1331 | + if not os.path.exists(rundir): |
1332 | + os.makedirs(rundir) |
1333 | + if not os.path.exists(logdir): |
1334 | + os.makedirs(logdir) |
1335 | + if not os.path.exists(basedir + "/data"): |
1336 | + shutil.copytree('/usr/share/moin/data', basedir + "/data") |
1337 | + if not os.path.exists(basedir + "/underlay"): |
1338 | + shutil.copytree('/usr/share/moin/underlay', basedir + "/underlay") |
1339 | + if not os.path.exists(staticdir): |
1340 | + shutil.copytree('/usr/share/moin/htdocs', staticdir) |
1341 | + |
1342 | + |
1343 | +@hooks.hook("config-changed") |
1344 | +def config_changed(): |
1345 | + # See if the port has changed |
1346 | + if (os.path.exists(gunicornconf)): |
1347 | + for line in open(gunicornconf): |
1348 | + if "--bind" in line: |
1349 | + port = line.split("=")[1].split(":")[1].rstrip("',\r\n") |
1350 | + break |
1351 | + if (listen_port != port): |
1352 | + close_port(port) |
1353 | + open_port(listen_port) |
1354 | + |
1355 | + # Create files from templates |
1356 | + from jinja2 import Environment, FileSystemLoader |
1357 | + template_env = Environment( |
1358 | + loader=FileSystemLoader(os.path.join(os.environ['CHARM_DIR'], |
1359 | + 'templates'))) |
1360 | + templ_vars = { |
1361 | + 'basedir': basedir, |
1362 | + 'unit_name': unit_name, |
1363 | + 'wiki_name': wiki_name, |
1364 | + 'admin_name': admin_name, |
1365 | + 'languages': languages, |
1366 | + 'loglevel': loglevel, |
1367 | + 'xapian_search': xapian_search, |
1368 | + 'listen_port': listen_port, |
1369 | + 'use_openid': use_openid, |
1370 | + 'openidrp_authorized_teams': openidrp_authorized_teams, |
1371 | + 'extra_settings': extra_settings, |
1372 | + } |
1373 | + |
1374 | + # gunicorn.conf.tmpl logging.conf.tmpl wikiconfig.py.tmpl wsgi.py.tmpl |
1375 | + wikiconfigtemplate = \ |
1376 | + template_env.get_template('wikiconfig.py.tmpl').render(templ_vars) |
1377 | + with open(wikiconfig, 'w') as wikiconfig_config: |
1378 | + wikiconfig_config.write(str(wikiconfigtemplate)) |
1379 | + wsgipytemplate = \ |
1380 | + template_env.get_template('wsgi.py.tmpl').render(templ_vars) |
1381 | + with open(wsgipy, 'w') as wsgipy_config: |
1382 | + wsgipy_config.write(str(wsgipytemplate)) |
1383 | + loggingconftemplate = \ |
1384 | + template_env.get_template('logging.conf.tmpl').render(templ_vars) |
1385 | + with open(loggingconf, 'w') as loggingconf_config: |
1386 | + loggingconf_config.write(str(loggingconftemplate)) |
1387 | + gunicornconftemplate = \ |
1388 | + template_env.get_template('gunicorn.conf.tmpl').render(templ_vars) |
1389 | + with open(gunicornconf, 'w') as gunicornconf_config: |
1390 | + gunicornconf_config.write(str(gunicornconftemplate)) |
1391 | + |
1392 | + # Handle xapian search updates |
1393 | + if (xapian_search == "True"): |
1394 | + if not os.path.exists(xapian_initialised): |
1395 | + subprocess.call(['moin', "--config-dir=%s/" % (basedir), 'index', |
1396 | + 'build', '--mode=add']) |
1397 | + open(xapian_initialised, 'w').close() |
1398 | + else: |
1399 | + subprocess.call(['moin', "--config=dir=%s/" % (basedir), 'index', |
1400 | + 'build', '--mode=rebuild']) |
1401 | + |
1402 | + # Install languages |
1403 | + for lang in languages.split(): |
1404 | + subprocess.call(['python', '-m', 'MoinMoin.packages', 'i', |
1405 | + "%s/underlay/pages/LanguageSetup/attachments/%s--all_pages.zip" |
1406 | + % (basedir, lang)], cwd=basedir) |
1407 | + |
1408 | + # fix perms to be owned by root:www-data and g+rwX |
1409 | + root_uid = pwd.getpwnam('root').pw_uid |
1410 | + www_gid = grp.getgrnam('www-data').gr_gid |
1411 | + for root, dirs, files in os.walk(basedir): |
1412 | + for d in dirs: |
1413 | + os.chown(os.path.join(root, d), root_uid, www_gid) |
1414 | + os.chmod(os.path.join(root, d), 0775) |
1415 | + for f in files: |
1416 | + os.chown(os.path.join(root, f), root_uid, www_gid) |
1417 | + os.chmod(os.path.join(root, f), 0664) |
1418 | + |
1419 | + # if we are using openid make sure files are in place |
1420 | + openid_python_path = "/usr/share/pyshared/MoinMoin/auth/openidrp_ext" |
1421 | + dist_dir = distutils.sysconfig.get_python_lib() |
1422 | + if (use_openid == "True"): |
1423 | + # Copy the openidrp.py file in place |
1424 | + shutil.copyfile("files/openidrp.py", |
1425 | + "%s/MoinMoin/auth/openidrp.py" % (dist_dir)) |
1426 | + # Copy the openidrp_teams.py file in place |
1427 | + shutil.copyfile("files/openidrp_teams.py", |
1428 | + "%s/openidrp_teams.py" % (openid_python_path)) |
1429 | + # Only copy and symlink the teams.py in place if we don't have it |
1430 | + if not os.path.exists("%s/teams.py" % (openid_python_path)): |
1431 | + shutil.copyfile("files/teams.py", "%s/teams.py" |
1432 | + % (openid_python_path)) |
1433 | + os.symlink("%s/teams.py" % (openid_python_path), |
1434 | + "%s/MoinMoin/auth/openidrp_ext/teams.py" % (dist_dir)) |
1435 | + |
1436 | + service_restart('gunicorn') |
1437 | + |
1438 | + |
1439 | +@hooks.hook("website-relation-joined") |
1440 | +def website_relation_join(): |
1441 | + relation_set(port=listen_port, hostname=hostname) |
1442 | + |
1443 | + |
1444 | +@hooks.hook("upgrade-charm") |
1445 | +def upgrade_charm(): |
1446 | + install() |
1447 | + |
1448 | + |
1449 | +@hooks.hook("start") |
1450 | +def start(): |
1451 | + service_start('gunicorn') |
1452 | + |
1453 | + |
1454 | +@hooks.hook("stop") |
1455 | +def stop(): |
1456 | + service_stop('gunicorn') |
1457 | + |
1458 | + |
1459 | +if __name__ == "__main__": |
1460 | + hooks.execute(sys.argv) |
1461 | |
1462 | === added symlink 'hooks/install' |
1463 | === target is u'hooks.py' |
1464 | === removed file 'hooks/install' |
1465 | --- hooks/install 2013-05-07 16:36:59 +0000 |
1466 | +++ hooks/install 1970-01-01 00:00:00 +0000 |
1467 | @@ -1,19 +0,0 @@ |
1468 | -#!/bin/bash |
1469 | - |
1470 | -UNIT_NAME=`echo $JUJU_UNIT_NAME | cut -d/ -f1` |
1471 | - |
1472 | -# --no-install-recommends and not fckeditor: because we don't want apache2 here. |
1473 | - |
1474 | -apt-get install --no-install-recommends -y python-moinmoin python-flup \ |
1475 | - gunicorn python-eventlet unzip python-openid python-xapian python-xappy \ |
1476 | - python-docutils python-cheetah |
1477 | - |
1478 | -# MoinMoin's command line script 'moin' check there first |
1479 | -# and we don't want that |
1480 | -rm /etc/moin/farmconfig.py |
1481 | - |
1482 | -mkdir -p /srv/${UNIT_NAME}/run |
1483 | -mkdir -p /srv/${UNIT_NAME}/logs |
1484 | - |
1485 | -cp -r /usr/share/moin/data /usr/share/moin/underlay /srv/${UNIT_NAME}/ |
1486 | -cp -r /usr/share/moin/htdocs /srv/${UNIT_NAME}/moin_static |
1487 | |
1488 | === added symlink 'hooks/start' |
1489 | === target is u'hooks.py' |
1490 | === removed file 'hooks/start' |
1491 | --- hooks/start 2011-11-01 20:33:21 +0000 |
1492 | +++ hooks/start 1970-01-01 00:00:00 +0000 |
1493 | @@ -1,4 +0,0 @@ |
1494 | -#!/bin/bash |
1495 | -# Here put anything that is needed to start the service. |
1496 | -# Note that currently this is run directly after install |
1497 | -# i.e. 'service apache2 start' |
1498 | |
1499 | === added symlink 'hooks/stop' |
1500 | === target is u'hooks.py' |
1501 | === removed file 'hooks/stop' |
1502 | --- hooks/stop 2011-11-01 20:33:21 +0000 |
1503 | +++ hooks/stop 1970-01-01 00:00:00 +0000 |
1504 | @@ -1,7 +0,0 @@ |
1505 | -#!/bin/bash |
1506 | -# This will be run when the service is being torn down, allowing you to disable |
1507 | -# it in various ways.. |
1508 | -# For example, if your web app uses a text file to signal to the load balancer |
1509 | -# that it is live... you could remove it and sleep for a bit to allow the load |
1510 | -# balancer to stop sending traffic. |
1511 | -# rm /srv/webroot/server-live.txt && sleep 30 |
1512 | |
1513 | === added symlink 'hooks/upgrade-charm' |
1514 | === target is u'hooks.py' |
1515 | === added symlink 'hooks/website-relation-joined' |
1516 | === target is u'hooks.py' |
1517 | === removed file 'hooks/website-relation-joined' |
1518 | --- hooks/website-relation-joined 2013-05-07 16:03:30 +0000 |
1519 | +++ hooks/website-relation-joined 1970-01-01 00:00:00 +0000 |
1520 | @@ -1,2 +0,0 @@ |
1521 | -#!/bin/sh |
1522 | -relation-set port=$(config-get listen_port) hostname=$(hostname -f) |
1523 | |
1524 | === added directory 'lib' |
1525 | === added directory 'lib/charm-helpers' |
1526 | === added file 'lib/charm-helpers/LICENSE.txt' |
1527 | --- lib/charm-helpers/LICENSE.txt 1970-01-01 00:00:00 +0000 |
1528 | +++ lib/charm-helpers/LICENSE.txt 2013-09-26 06:24:47 +0000 |
1529 | @@ -0,0 +1,661 @@ |
1530 | + GNU AFFERO GENERAL PUBLIC LICENSE |
1531 | + Version 3, 19 November 2007 |
1532 | + |
1533 | + Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> |
1534 | + Everyone is permitted to copy and distribute verbatim copies |
1535 | + of this license document, but changing it is not allowed. |
1536 | + |
1537 | + Preamble |
1538 | + |
1539 | + The GNU Affero General Public License is a free, copyleft license for |
1540 | +software and other kinds of works, specifically designed to ensure |
1541 | +cooperation with the community in the case of network server software. |
1542 | + |
1543 | + The licenses for most software and other practical works are designed |
1544 | +to take away your freedom to share and change the works. By contrast, |
1545 | +our General Public Licenses are intended to guarantee your freedom to |
1546 | +share and change all versions of a program--to make sure it remains free |
1547 | +software for all its users. |
1548 | + |
1549 | + When we speak of free software, we are referring to freedom, not |
1550 | +price. Our General Public Licenses are designed to make sure that you |
1551 | +have the freedom to distribute copies of free software (and charge for |
1552 | +them if you wish), that you receive source code or can get it if you |
1553 | +want it, that you can change the software or use pieces of it in new |
1554 | +free programs, and that you know you can do these things. |
1555 | + |
1556 | + Developers that use our General Public Licenses protect your rights |
1557 | +with two steps: (1) assert copyright on the software, and (2) offer |
1558 | +you this License which gives you legal permission to copy, distribute |
1559 | +and/or modify the software. |
1560 | + |
1561 | + A secondary benefit of defending all users' freedom is that |
1562 | +improvements made in alternate versions of the program, if they |
1563 | +receive widespread use, become available for other developers to |
1564 | +incorporate. Many developers of free software are heartened and |
1565 | +encouraged by the resulting cooperation. However, in the case of |
1566 | +software used on network servers, this result may fail to come about. |
1567 | +The GNU General Public License permits making a modified version and |
1568 | +letting the public access it on a server without ever releasing its |
1569 | +source code to the public. |
1570 | + |
1571 | + The GNU Affero General Public License is designed specifically to |
1572 | +ensure that, in such cases, the modified source code becomes available |
1573 | +to the community. It requires the operator of a network server to |
1574 | +provide the source code of the modified version running there to the |
1575 | +users of that server. Therefore, public use of a modified version, on |
1576 | +a publicly accessible server, gives the public access to the source |
1577 | +code of the modified version. |
1578 | + |
1579 | + An older license, called the Affero General Public License and |
1580 | +published by Affero, was designed to accomplish similar goals. This is |
1581 | +a different license, not a version of the Affero GPL, but Affero has |
1582 | +released a new version of the Affero GPL which permits relicensing under |
1583 | +this license. |
1584 | + |
1585 | + The precise terms and conditions for copying, distribution and |
1586 | +modification follow. |
1587 | + |
1588 | + TERMS AND CONDITIONS |
1589 | + |
1590 | + 0. Definitions. |
1591 | + |
1592 | + "This License" refers to version 3 of the GNU Affero General Public License. |
1593 | + |
1594 | + "Copyright" also means copyright-like laws that apply to other kinds of |
1595 | +works, such as semiconductor masks. |
1596 | + |
1597 | + "The Program" refers to any copyrightable work licensed under this |
1598 | +License. Each licensee is addressed as "you". "Licensees" and |
1599 | +"recipients" may be individuals or organizations. |
1600 | + |
1601 | + To "modify" a work means to copy from or adapt all or part of the work |
1602 | +in a fashion requiring copyright permission, other than the making of an |
1603 | +exact copy. The resulting work is called a "modified version" of the |
1604 | +earlier work or a work "based on" the earlier work. |
1605 | + |
1606 | + A "covered work" means either the unmodified Program or a work based |
1607 | +on the Program. |
1608 | + |
1609 | + To "propagate" a work means to do anything with it that, without |
1610 | +permission, would make you directly or secondarily liable for |
1611 | +infringement under applicable copyright law, except executing it on a |
1612 | +computer or modifying a private copy. Propagation includes copying, |
1613 | +distribution (with or without modification), making available to the |
1614 | +public, and in some countries other activities as well. |
1615 | + |
1616 | + To "convey" a work means any kind of propagation that enables other |
1617 | +parties to make or receive copies. Mere interaction with a user through |
1618 | +a computer network, with no transfer of a copy, is not conveying. |
1619 | + |
1620 | + An interactive user interface displays "Appropriate Legal Notices" |
1621 | +to the extent that it includes a convenient and prominently visible |
1622 | +feature that (1) displays an appropriate copyright notice, and (2) |
1623 | +tells the user that there is no warranty for the work (except to the |
1624 | +extent that warranties are provided), that licensees may convey the |
1625 | +work under this License, and how to view a copy of this License. If |
1626 | +the interface presents a list of user commands or options, such as a |
1627 | +menu, a prominent item in the list meets this criterion. |
1628 | + |
1629 | + 1. Source Code. |
1630 | + |
1631 | + The "source code" for a work means the preferred form of the work |
1632 | +for making modifications to it. "Object code" means any non-source |
1633 | +form of a work. |
1634 | + |
1635 | + A "Standard Interface" means an interface that either is an official |
1636 | +standard defined by a recognized standards body, or, in the case of |
1637 | +interfaces specified for a particular programming language, one that |
1638 | +is widely used among developers working in that language. |
1639 | + |
1640 | + The "System Libraries" of an executable work include anything, other |
1641 | +than the work as a whole, that (a) is included in the normal form of |
1642 | +packaging a Major Component, but which is not part of that Major |
1643 | +Component, and (b) serves only to enable use of the work with that |
1644 | +Major Component, or to implement a Standard Interface for which an |
1645 | +implementation is available to the public in source code form. A |
1646 | +"Major Component", in this context, means a major essential component |
1647 | +(kernel, window system, and so on) of the specific operating system |
1648 | +(if any) on which the executable work runs, or a compiler used to |
1649 | +produce the work, or an object code interpreter used to run it. |
1650 | + |
1651 | + The "Corresponding Source" for a work in object code form means all |
1652 | +the source code needed to generate, install, and (for an executable |
1653 | +work) run the object code and to modify the work, including scripts to |
1654 | +control those activities. However, it does not include the work's |
1655 | +System Libraries, or general-purpose tools or generally available free |
1656 | +programs which are used unmodified in performing those activities but |
1657 | +which are not part of the work. For example, Corresponding Source |
1658 | +includes interface definition files associated with source files for |
1659 | +the work, and the source code for shared libraries and dynamically |
1660 | +linked subprograms that the work is specifically designed to require, |
1661 | +such as by intimate data communication or control flow between those |
1662 | +subprograms and other parts of the work. |
1663 | + |
1664 | + The Corresponding Source need not include anything that users |
1665 | +can regenerate automatically from other parts of the Corresponding |
1666 | +Source. |
1667 | + |
1668 | + The Corresponding Source for a work in source code form is that |
1669 | +same work. |
1670 | + |
1671 | + 2. Basic Permissions. |
1672 | + |
1673 | + All rights granted under this License are granted for the term of |
1674 | +copyright on the Program, and are irrevocable provided the stated |
1675 | +conditions are met. This License explicitly affirms your unlimited |
1676 | +permission to run the unmodified Program. The output from running a |
1677 | +covered work is covered by this License only if the output, given its |
1678 | +content, constitutes a covered work. This License acknowledges your |
1679 | +rights of fair use or other equivalent, as provided by copyright law. |
1680 | + |
1681 | + You may make, run and propagate covered works that you do not |
1682 | +convey, without conditions so long as your license otherwise remains |
1683 | +in force. You may convey covered works to others for the sole purpose |
1684 | +of having them make modifications exclusively for you, or provide you |
1685 | +with facilities for running those works, provided that you comply with |
1686 | +the terms of this License in conveying all material for which you do |
1687 | +not control copyright. Those thus making or running the covered works |
1688 | +for you must do so exclusively on your behalf, under your direction |
1689 | +and control, on terms that prohibit them from making any copies of |
1690 | +your copyrighted material outside their relationship with you. |
1691 | + |
1692 | + Conveying under any other circumstances is permitted solely under |
1693 | +the conditions stated below. Sublicensing is not allowed; section 10 |
1694 | +makes it unnecessary. |
1695 | + |
1696 | + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. |
1697 | + |
1698 | + No covered work shall be deemed part of an effective technological |
1699 | +measure under any applicable law fulfilling obligations under article |
1700 | +11 of the WIPO copyright treaty adopted on 20 December 1996, or |
1701 | +similar laws prohibiting or restricting circumvention of such |
1702 | +measures. |
1703 | + |
1704 | + When you convey a covered work, you waive any legal power to forbid |
1705 | +circumvention of technological measures to the extent such circumvention |
1706 | +is effected by exercising rights under this License with respect to |
1707 | +the covered work, and you disclaim any intention to limit operation or |
1708 | +modification of the work as a means of enforcing, against the work's |
1709 | +users, your or third parties' legal rights to forbid circumvention of |
1710 | +technological measures. |
1711 | + |
1712 | + 4. Conveying Verbatim Copies. |
1713 | + |
1714 | + You may convey verbatim copies of the Program's source code as you |
1715 | +receive it, in any medium, provided that you conspicuously and |
1716 | +appropriately publish on each copy an appropriate copyright notice; |
1717 | +keep intact all notices stating that this License and any |
1718 | +non-permissive terms added in accord with section 7 apply to the code; |
1719 | +keep intact all notices of the absence of any warranty; and give all |
1720 | +recipients a copy of this License along with the Program. |
1721 | + |
1722 | + You may charge any price or no price for each copy that you convey, |
1723 | +and you may offer support or warranty protection for a fee. |
1724 | + |
1725 | + 5. Conveying Modified Source Versions. |
1726 | + |
1727 | + You may convey a work based on the Program, or the modifications to |
1728 | +produce it from the Program, in the form of source code under the |
1729 | +terms of section 4, provided that you also meet all of these conditions: |
1730 | + |
1731 | + a) The work must carry prominent notices stating that you modified |
1732 | + it, and giving a relevant date. |
1733 | + |
1734 | + b) The work must carry prominent notices stating that it is |
1735 | + released under this License and any conditions added under section |
1736 | + 7. This requirement modifies the requirement in section 4 to |
1737 | + "keep intact all notices". |
1738 | + |
1739 | + c) You must license the entire work, as a whole, under this |
1740 | + License to anyone who comes into possession of a copy. This |
1741 | + License will therefore apply, along with any applicable section 7 |
1742 | + additional terms, to the whole of the work, and all its parts, |
1743 | + regardless of how they are packaged. This License gives no |
1744 | + permission to license the work in any other way, but it does not |
1745 | + invalidate such permission if you have separately received it. |
1746 | + |
1747 | + d) If the work has interactive user interfaces, each must display |
1748 | + Appropriate Legal Notices; however, if the Program has interactive |
1749 | + interfaces that do not display Appropriate Legal Notices, your |
1750 | + work need not make them do so. |
1751 | + |
1752 | + A compilation of a covered work with other separate and independent |
1753 | +works, which are not by their nature extensions of the covered work, |
1754 | +and which are not combined with it such as to form a larger program, |
1755 | +in or on a volume of a storage or distribution medium, is called an |
1756 | +"aggregate" if the compilation and its resulting copyright are not |
1757 | +used to limit the access or legal rights of the compilation's users |
1758 | +beyond what the individual works permit. Inclusion of a covered work |
1759 | +in an aggregate does not cause this License to apply to the other |
1760 | +parts of the aggregate. |
1761 | + |
1762 | + 6. Conveying Non-Source Forms. |
1763 | + |
1764 | + You may convey a covered work in object code form under the terms |
1765 | +of sections 4 and 5, provided that you also convey the |
1766 | +machine-readable Corresponding Source under the terms of this License, |
1767 | +in one of these ways: |
1768 | + |
1769 | + a) Convey the object code in, or embodied in, a physical product |
1770 | + (including a physical distribution medium), accompanied by the |
1771 | + Corresponding Source fixed on a durable physical medium |
1772 | + customarily used for software interchange. |
1773 | + |
1774 | + b) Convey the object code in, or embodied in, a physical product |
1775 | + (including a physical distribution medium), accompanied by a |
1776 | + written offer, valid for at least three years and valid for as |
1777 | + long as you offer spare parts or customer support for that product |
1778 | + model, to give anyone who possesses the object code either (1) a |
1779 | + copy of the Corresponding Source for all the software in the |
1780 | + product that is covered by this License, on a durable physical |
1781 | + medium customarily used for software interchange, for a price no |
1782 | + more than your reasonable cost of physically performing this |
1783 | + conveying of source, or (2) access to copy the |
1784 | + Corresponding Source from a network server at no charge. |
1785 | + |
1786 | + c) Convey individual copies of the object code with a copy of the |
1787 | + written offer to provide the Corresponding Source. This |
1788 | + alternative is allowed only occasionally and noncommercially, and |
1789 | + only if you received the object code with such an offer, in accord |
1790 | + with subsection 6b. |
1791 | + |
1792 | + d) Convey the object code by offering access from a designated |
1793 | + place (gratis or for a charge), and offer equivalent access to the |
1794 | + Corresponding Source in the same way through the same place at no |
1795 | + further charge. You need not require recipients to copy the |
1796 | + Corresponding Source along with the object code. If the place to |
1797 | + copy the object code is a network server, the Corresponding Source |
1798 | + may be on a different server (operated by you or a third party) |
1799 | + that supports equivalent copying facilities, provided you maintain |
1800 | + clear directions next to the object code saying where to find the |
1801 | + Corresponding Source. Regardless of what server hosts the |
1802 | + Corresponding Source, you remain obligated to ensure that it is |
1803 | + available for as long as needed to satisfy these requirements. |
1804 | + |
1805 | + e) Convey the object code using peer-to-peer transmission, provided |
1806 | + you inform other peers where the object code and Corresponding |
1807 | + Source of the work are being offered to the general public at no |
1808 | + charge under subsection 6d. |
1809 | + |
1810 | + A separable portion of the object code, whose source code is excluded |
1811 | +from the Corresponding Source as a System Library, need not be |
1812 | +included in conveying the object code work. |
1813 | + |
1814 | + A "User Product" is either (1) a "consumer product", which means any |
1815 | +tangible personal property which is normally used for personal, family, |
1816 | +or household purposes, or (2) anything designed or sold for incorporation |
1817 | +into a dwelling. In determining whether a product is a consumer product, |
1818 | +doubtful cases shall be resolved in favor of coverage. For a particular |
1819 | +product received by a particular user, "normally used" refers to a |
1820 | +typical or common use of that class of product, regardless of the status |
1821 | +of the particular user or of the way in which the particular user |
1822 | +actually uses, or expects or is expected to use, the product. A product |
1823 | +is a consumer product regardless of whether the product has substantial |
1824 | +commercial, industrial or non-consumer uses, unless such uses represent |
1825 | +the only significant mode of use of the product. |
1826 | + |
1827 | + "Installation Information" for a User Product means any methods, |
1828 | +procedures, authorization keys, or other information required to install |
1829 | +and execute modified versions of a covered work in that User Product from |
1830 | +a modified version of its Corresponding Source. The information must |
1831 | +suffice to ensure that the continued functioning of the modified object |
1832 | +code is in no case prevented or interfered with solely because |
1833 | +modification has been made. |
1834 | + |
1835 | + If you convey an object code work under this section in, or with, or |
1836 | +specifically for use in, a User Product, and the conveying occurs as |
1837 | +part of a transaction in which the right of possession and use of the |
1838 | +User Product is transferred to the recipient in perpetuity or for a |
1839 | +fixed term (regardless of how the transaction is characterized), the |
1840 | +Corresponding Source conveyed under this section must be accompanied |
1841 | +by the Installation Information. But this requirement does not apply |
1842 | +if neither you nor any third party retains the ability to install |
1843 | +modified object code on the User Product (for example, the work has |
1844 | +been installed in ROM). |
1845 | + |
1846 | + The requirement to provide Installation Information does not include a |
1847 | +requirement to continue to provide support service, warranty, or updates |
1848 | +for a work that has been modified or installed by the recipient, or for |
1849 | +the User Product in which it has been modified or installed. Access to a |
1850 | +network may be denied when the modification itself materially and |
1851 | +adversely affects the operation of the network or violates the rules and |
1852 | +protocols for communication across the network. |
1853 | + |
1854 | + Corresponding Source conveyed, and Installation Information provided, |
1855 | +in accord with this section must be in a format that is publicly |
1856 | +documented (and with an implementation available to the public in |
1857 | +source code form), and must require no special password or key for |
1858 | +unpacking, reading or copying. |
1859 | + |
1860 | + 7. Additional Terms. |
1861 | + |
1862 | + "Additional permissions" are terms that supplement the terms of this |
1863 | +License by making exceptions from one or more of its conditions. |
1864 | +Additional permissions that are applicable to the entire Program shall |
1865 | +be treated as though they were included in this License, to the extent |
1866 | +that they are valid under applicable law. If additional permissions |
1867 | +apply only to part of the Program, that part may be used separately |
1868 | +under those permissions, but the entire Program remains governed by |
1869 | +this License without regard to the additional permissions. |
1870 | + |
1871 | + When you convey a copy of a covered work, you may at your option |
1872 | +remove any additional permissions from that copy, or from any part of |
1873 | +it. (Additional permissions may be written to require their own |
1874 | +removal in certain cases when you modify the work.) You may place |
1875 | +additional permissions on material, added by you to a covered work, |
1876 | +for which you have or can give appropriate copyright permission. |
1877 | + |
1878 | + Notwithstanding any other provision of this License, for material you |
1879 | +add to a covered work, you may (if authorized by the copyright holders of |
1880 | +that material) supplement the terms of this License with terms: |
1881 | + |
1882 | + a) Disclaiming warranty or limiting liability differently from the |
1883 | + terms of sections 15 and 16 of this License; or |
1884 | + |
1885 | + b) Requiring preservation of specified reasonable legal notices or |
1886 | + author attributions in that material or in the Appropriate Legal |
1887 | + Notices displayed by works containing it; or |
1888 | + |
1889 | + c) Prohibiting misrepresentation of the origin of that material, or |
1890 | + requiring that modified versions of such material be marked in |
1891 | + reasonable ways as different from the original version; or |
1892 | + |
1893 | + d) Limiting the use for publicity purposes of names of licensors or |
1894 | + authors of the material; or |
1895 | + |
1896 | + e) Declining to grant rights under trademark law for use of some |
1897 | + trade names, trademarks, or service marks; or |
1898 | + |
1899 | + f) Requiring indemnification of licensors and authors of that |
1900 | + material by anyone who conveys the material (or modified versions of |
1901 | + it) with contractual assumptions of liability to the recipient, for |
1902 | + any liability that these contractual assumptions directly impose on |
1903 | + those licensors and authors. |
1904 | + |
1905 | + All other non-permissive additional terms are considered "further |
1906 | +restrictions" within the meaning of section 10. If the Program as you |
1907 | +received it, or any part of it, contains a notice stating that it is |
1908 | +governed by this License along with a term that is a further |
1909 | +restriction, you may remove that term. If a license document contains |
1910 | +a further restriction but permits relicensing or conveying under this |
1911 | +License, you may add to a covered work material governed by the terms |
1912 | +of that license document, provided that the further restriction does |
1913 | +not survive such relicensing or conveying. |
1914 | + |
1915 | + If you add terms to a covered work in accord with this section, you |
1916 | +must place, in the relevant source files, a statement of the |
1917 | +additional terms that apply to those files, or a notice indicating |
1918 | +where to find the applicable terms. |
1919 | + |
1920 | + Additional terms, permissive or non-permissive, may be stated in the |
1921 | +form of a separately written license, or stated as exceptions; |
1922 | +the above requirements apply either way. |
1923 | + |
1924 | + 8. Termination. |
1925 | + |
1926 | + You may not propagate or modify a covered work except as expressly |
1927 | +provided under this License. Any attempt otherwise to propagate or |
1928 | +modify it is void, and will automatically terminate your rights under |
1929 | +this License (including any patent licenses granted under the third |
1930 | +paragraph of section 11). |
1931 | + |
1932 | + However, if you cease all violation of this License, then your |
1933 | +license from a particular copyright holder is reinstated (a) |
1934 | +provisionally, unless and until the copyright holder explicitly and |
1935 | +finally terminates your license, and (b) permanently, if the copyright |
1936 | +holder fails to notify you of the violation by some reasonable means |
1937 | +prior to 60 days after the cessation. |
1938 | + |
1939 | + Moreover, your license from a particular copyright holder is |
1940 | +reinstated permanently if the copyright holder notifies you of the |
1941 | +violation by some reasonable means, this is the first time you have |
1942 | +received notice of violation of this License (for any work) from that |
1943 | +copyright holder, and you cure the violation prior to 30 days after |
1944 | +your receipt of the notice. |
1945 | + |
1946 | + Termination of your rights under this section does not terminate the |
1947 | +licenses of parties who have received copies or rights from you under |
1948 | +this License. If your rights have been terminated and not permanently |
1949 | +reinstated, you do not qualify to receive new licenses for the same |
1950 | +material under section 10. |
1951 | + |
1952 | + 9. Acceptance Not Required for Having Copies. |
1953 | + |
1954 | + You are not required to accept this License in order to receive or |
1955 | +run a copy of the Program. Ancillary propagation of a covered work |
1956 | +occurring solely as a consequence of using peer-to-peer transmission |
1957 | +to receive a copy likewise does not require acceptance. However, |
1958 | +nothing other than this License grants you permission to propagate or |
1959 | +modify any covered work. These actions infringe copyright if you do |
1960 | +not accept this License. Therefore, by modifying or propagating a |
1961 | +covered work, you indicate your acceptance of this License to do so. |
1962 | + |
1963 | + 10. Automatic Licensing of Downstream Recipients. |
1964 | + |
1965 | + Each time you convey a covered work, the recipient automatically |
1966 | +receives a license from the original licensors, to run, modify and |
1967 | +propagate that work, subject to this License. You are not responsible |
1968 | +for enforcing compliance by third parties with this License. |
1969 | + |
1970 | + An "entity transaction" is a transaction transferring control of an |
1971 | +organization, or substantially all assets of one, or subdividing an |
1972 | +organization, or merging organizations. If propagation of a covered |
1973 | +work results from an entity transaction, each party to that |
1974 | +transaction who receives a copy of the work also receives whatever |
1975 | +licenses to the work the party's predecessor in interest had or could |
1976 | +give under the previous paragraph, plus a right to possession of the |
1977 | +Corresponding Source of the work from the predecessor in interest, if |
1978 | +the predecessor has it or can get it with reasonable efforts. |
1979 | + |
1980 | + You may not impose any further restrictions on the exercise of the |
1981 | +rights granted or affirmed under this License. For example, you may |
1982 | +not impose a license fee, royalty, or other charge for exercise of |
1983 | +rights granted under this License, and you may not initiate litigation |
1984 | +(including a cross-claim or counterclaim in a lawsuit) alleging that |
1985 | +any patent claim is infringed by making, using, selling, offering for |
1986 | +sale, or importing the Program or any portion of it. |
1987 | + |
1988 | + 11. Patents. |
1989 | + |
1990 | + A "contributor" is a copyright holder who authorizes use under this |
1991 | +License of the Program or a work on which the Program is based. The |
1992 | +work thus licensed is called the contributor's "contributor version". |
1993 | + |
1994 | + A contributor's "essential patent claims" are all patent claims |
1995 | +owned or controlled by the contributor, whether already acquired or |
1996 | +hereafter acquired, that would be infringed by some manner, permitted |
1997 | +by this License, of making, using, or selling its contributor version, |
1998 | +but do not include claims that would be infringed only as a |
1999 | +consequence of further modification of the contributor version. For |
2000 | +purposes of this definition, "control" includes the right to grant |
2001 | +patent sublicenses in a manner consistent with the requirements of |
2002 | +this License. |
2003 | + |
2004 | + Each contributor grants you a non-exclusive, worldwide, royalty-free |
2005 | +patent license under the contributor's essential patent claims, to |
2006 | +make, use, sell, offer for sale, import and otherwise run, modify and |
2007 | +propagate the contents of its contributor version. |
2008 | + |
2009 | + In the following three paragraphs, a "patent license" is any express |
2010 | +agreement or commitment, however denominated, not to enforce a patent |
2011 | +(such as an express permission to practice a patent or covenant not to |
2012 | +sue for patent infringement). To "grant" such a patent license to a |
2013 | +party means to make such an agreement or commitment not to enforce a |
2014 | +patent against the party. |
2015 | + |
2016 | + If you convey a covered work, knowingly relying on a patent license, |
2017 | +and the Corresponding Source of the work is not available for anyone |
2018 | +to copy, free of charge and under the terms of this License, through a |
2019 | +publicly available network server or other readily accessible means, |
2020 | +then you must either (1) cause the Corresponding Source to be so |
2021 | +available, or (2) arrange to deprive yourself of the benefit of the |
2022 | +patent license for this particular work, or (3) arrange, in a manner |
2023 | +consistent with the requirements of this License, to extend the patent |
2024 | +license to downstream recipients. "Knowingly relying" means you have |
2025 | +actual knowledge that, but for the patent license, your conveying the |
2026 | +covered work in a country, or your recipient's use of the covered work |
2027 | +in a country, would infringe one or more identifiable patents in that |
2028 | +country that you have reason to believe are valid. |
2029 | + |
2030 | + If, pursuant to or in connection with a single transaction or |
2031 | +arrangement, you convey, or propagate by procuring conveyance of, a |
2032 | +covered work, and grant a patent license to some of the parties |
2033 | +receiving the covered work authorizing them to use, propagate, modify |
2034 | +or convey a specific copy of the covered work, then the patent license |
2035 | +you grant is automatically extended to all recipients of the covered |
2036 | +work and works based on it. |
2037 | + |
2038 | + A patent license is "discriminatory" if it does not include within |
2039 | +the scope of its coverage, prohibits the exercise of, or is |
2040 | +conditioned on the non-exercise of one or more of the rights that are |
2041 | +specifically granted under this License. You may not convey a covered |
2042 | +work if you are a party to an arrangement with a third party that is |
2043 | +in the business of distributing software, under which you make payment |
2044 | +to the third party based on the extent of your activity of conveying |
2045 | +the work, and under which the third party grants, to any of the |
2046 | +parties who would receive the covered work from you, a discriminatory |
2047 | +patent license (a) in connection with copies of the covered work |
2048 | +conveyed by you (or copies made from those copies), or (b) primarily |
2049 | +for and in connection with specific products or compilations that |
2050 | +contain the covered work, unless you entered into that arrangement, |
2051 | +or that patent license was granted, prior to 28 March 2007. |
2052 | + |
2053 | + Nothing in this License shall be construed as excluding or limiting |
2054 | +any implied license or other defenses to infringement that may |
2055 | +otherwise be available to you under applicable patent law. |
2056 | + |
2057 | + 12. No Surrender of Others' Freedom. |
2058 | + |
2059 | + If conditions are imposed on you (whether by court order, agreement or |
2060 | +otherwise) that contradict the conditions of this License, they do not |
2061 | +excuse you from the conditions of this License. If you cannot convey a |
2062 | +covered work so as to satisfy simultaneously your obligations under this |
2063 | +License and any other pertinent obligations, then as a consequence you may |
2064 | +not convey it at all. For example, if you agree to terms that obligate you |
2065 | +to collect a royalty for further conveying from those to whom you convey |
2066 | +the Program, the only way you could satisfy both those terms and this |
2067 | +License would be to refrain entirely from conveying the Program. |
2068 | + |
2069 | + 13. Remote Network Interaction; Use with the GNU General Public License. |
2070 | + |
2071 | + Notwithstanding any other provision of this License, if you modify the |
2072 | +Program, your modified version must prominently offer all users |
2073 | +interacting with it remotely through a computer network (if your version |
2074 | +supports such interaction) an opportunity to receive the Corresponding |
2075 | +Source of your version by providing access to the Corresponding Source |
2076 | +from a network server at no charge, through some standard or customary |
2077 | +means of facilitating copying of software. This Corresponding Source |
2078 | +shall include the Corresponding Source for any work covered by version 3 |
2079 | +of the GNU General Public License that is incorporated pursuant to the |
2080 | +following paragraph. |
2081 | + |
2082 | + Notwithstanding any other provision of this License, you have |
2083 | +permission to link or combine any covered work with a work licensed |
2084 | +under version 3 of the GNU General Public License into a single |
2085 | +combined work, and to convey the resulting work. The terms of this |
2086 | +License will continue to apply to the part which is the covered work, |
2087 | +but the work with which it is combined will remain governed by version |
2088 | +3 of the GNU General Public License. |
2089 | + |
2090 | + 14. Revised Versions of this License. |
2091 | + |
2092 | + The Free Software Foundation may publish revised and/or new versions of |
2093 | +the GNU Affero General Public License from time to time. Such new versions |
2094 | +will be similar in spirit to the present version, but may differ in detail to |
2095 | +address new problems or concerns. |
2096 | + |
2097 | + Each version is given a distinguishing version number. If the |
2098 | +Program specifies that a certain numbered version of the GNU Affero General |
2099 | +Public License "or any later version" applies to it, you have the |
2100 | +option of following the terms and conditions either of that numbered |
2101 | +version or of any later version published by the Free Software |
2102 | +Foundation. If the Program does not specify a version number of the |
2103 | +GNU Affero General Public License, you may choose any version ever published |
2104 | +by the Free Software Foundation. |
2105 | + |
2106 | + If the Program specifies that a proxy can decide which future |
2107 | +versions of the GNU Affero General Public License can be used, that proxy's |
2108 | +public statement of acceptance of a version permanently authorizes you |
2109 | +to choose that version for the Program. |
2110 | + |
2111 | + Later license versions may give you additional or different |
2112 | +permissions. However, no additional obligations are imposed on any |
2113 | +author or copyright holder as a result of your choosing to follow a |
2114 | +later version. |
2115 | + |
2116 | + 15. Disclaimer of Warranty. |
2117 | + |
2118 | + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY |
2119 | +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT |
2120 | +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY |
2121 | +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, |
2122 | +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
2123 | +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM |
2124 | +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF |
2125 | +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. |
2126 | + |
2127 | + 16. Limitation of Liability. |
2128 | + |
2129 | + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING |
2130 | +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS |
2131 | +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY |
2132 | +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE |
2133 | +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF |
2134 | +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD |
2135 | +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), |
2136 | +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF |
2137 | +SUCH DAMAGES. |
2138 | + |
2139 | + 17. Interpretation of Sections 15 and 16. |
2140 | + |
2141 | + If the disclaimer of warranty and limitation of liability provided |
2142 | +above cannot be given local legal effect according to their terms, |
2143 | +reviewing courts shall apply local law that most closely approximates |
2144 | +an absolute waiver of all civil liability in connection with the |
2145 | +Program, unless a warranty or assumption of liability accompanies a |
2146 | +copy of the Program in return for a fee. |
2147 | + |
2148 | + END OF TERMS AND CONDITIONS |
2149 | + |
2150 | + How to Apply These Terms to Your New Programs |
2151 | + |
2152 | + If you develop a new program, and you want it to be of the greatest |
2153 | +possible use to the public, the best way to achieve this is to make it |
2154 | +free software which everyone can redistribute and change under these terms. |
2155 | + |
2156 | + To do so, attach the following notices to the program. It is safest |
2157 | +to attach them to the start of each source file to most effectively |
2158 | +state the exclusion of warranty; and each file should have at least |
2159 | +the "copyright" line and a pointer to where the full notice is found. |
2160 | + |
2161 | + <one line to give the program's name and a brief idea of what it does.> |
2162 | + Copyright (C) <year> <name of author> |
2163 | + |
2164 | + This program is free software: you can redistribute it and/or modify |
2165 | + it under the terms of the GNU Affero General Public License as published by |
2166 | + the Free Software Foundation, either version 3 of the License, or |
2167 | + (at your option) any later version. |
2168 | + |
2169 | + This program is distributed in the hope that it will be useful, |
2170 | + but WITHOUT ANY WARRANTY; without even the implied warranty of |
2171 | + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
2172 | + GNU Affero General Public License for more details. |
2173 | + |
2174 | + You should have received a copy of the GNU Affero General Public License |
2175 | + along with this program. If not, see <http://www.gnu.org/licenses/>. |
2176 | + |
2177 | +Also add information on how to contact you by electronic and paper mail. |
2178 | + |
2179 | + If your software can interact with users remotely through a computer |
2180 | +network, you should also make sure that it provides a way for users to |
2181 | +get its source. For example, if your program is a web application, its |
2182 | +interface could display a "Source" link that leads users to an archive |
2183 | +of the code. There are many ways you could offer source, and different |
2184 | +solutions will be better for different programs; see section 13 for the |
2185 | +specific requirements. |
2186 | + |
2187 | + You should also get your employer (if you work as a programmer) or school, |
2188 | +if any, to sign a "copyright disclaimer" for the program, if necessary. |
2189 | +For more information on this, and how to apply and follow the GNU AGPL, see |
2190 | +<http://www.gnu.org/licenses/>. |
2191 | |
2192 | === added file 'lib/charm-helpers/MANIFEST.in' |
2193 | --- lib/charm-helpers/MANIFEST.in 1970-01-01 00:00:00 +0000 |
2194 | +++ lib/charm-helpers/MANIFEST.in 2013-09-26 06:24:47 +0000 |
2195 | @@ -0,0 +1,6 @@ |
2196 | +include *.txt |
2197 | +include Makefile |
2198 | +include VERSION |
2199 | +include MANIFEST.in |
2200 | +include scripts/* |
2201 | +recursive-include debian * |
2202 | |
2203 | === added file 'lib/charm-helpers/Makefile' |
2204 | --- lib/charm-helpers/Makefile 1970-01-01 00:00:00 +0000 |
2205 | +++ lib/charm-helpers/Makefile 2013-09-26 06:24:47 +0000 |
2206 | @@ -0,0 +1,46 @@ |
2207 | +PROJECT=charmhelpers |
2208 | +PYTHON := /usr/bin/env python |
2209 | +SUITE=unstable |
2210 | +TESTS=tests/ |
2211 | + |
2212 | +all: |
2213 | + @echo "make source - Create source package" |
2214 | + @echo "make sdeb - Create debian source package" |
2215 | + @echo "make deb - Create debian package" |
2216 | + @echo "make clean" |
2217 | + @echo "make userinstall - Install locally" |
2218 | + |
2219 | +sdeb: source |
2220 | + scripts/build source |
2221 | + |
2222 | +deb: source |
2223 | + scripts/build |
2224 | + |
2225 | +source: setup.py |
2226 | + scripts/update-revno |
2227 | + python setup.py sdist |
2228 | + |
2229 | +clean: |
2230 | + python setup.py clean |
2231 | + rm -rf build/ MANIFEST |
2232 | + find . -name '*.pyc' -delete |
2233 | + rm -rf dist/* |
2234 | + dh_clean |
2235 | + |
2236 | +userinstall: |
2237 | + scripts/update-revno |
2238 | + python setup.py install --user |
2239 | + |
2240 | +test: |
2241 | + @echo Starting tests... |
2242 | + @$(PYTHON) /usr/bin/nosetests --nologcapture tests/ |
2243 | + |
2244 | +ftest: |
2245 | + @echo Starting fast tests... |
2246 | + @$(PYTHON) /usr/bin/nosetests --attr '!slow' --nologcapture tests/ |
2247 | + |
2248 | +lint: |
2249 | + @echo Checking for Python syntax... |
2250 | + @flake8 --ignore=E123,E501 $(PROJECT) $(TESTS) && echo OK |
2251 | + |
2252 | +build: test lint |
2253 | |
2254 | === added file 'lib/charm-helpers/README.test' |
2255 | --- lib/charm-helpers/README.test 1970-01-01 00:00:00 +0000 |
2256 | +++ lib/charm-helpers/README.test 2013-09-26 06:24:47 +0000 |
2257 | @@ -0,0 +1,7 @@ |
2258 | +Required Packages for Running Tests |
2259 | +----------------------------------- |
2260 | +python-shelltoolbox |
2261 | +python-tempita |
2262 | +python-nose |
2263 | +python-mock |
2264 | +python-testtools |
2265 | |
2266 | === added file 'lib/charm-helpers/README.txt' |
2267 | --- lib/charm-helpers/README.txt 1970-01-01 00:00:00 +0000 |
2268 | +++ lib/charm-helpers/README.txt 2013-09-26 06:24:47 +0000 |
2269 | @@ -0,0 +1,8 @@ |
2270 | +============ |
2271 | +CharmHelpers |
2272 | +============ |
2273 | + |
2274 | +CharmHelpers provides an opinionated set of tools for building Juju |
2275 | +charms that work together. In addition to basic tasks like interact- |
2276 | +ing with the charm environment and the machine it runs on, it also |
2277 | +helps keep you build hooks and establish relations effortlessly. |
2278 | |
2279 | === added file 'lib/charm-helpers/REVISION' |
2280 | --- lib/charm-helpers/REVISION 1970-01-01 00:00:00 +0000 |
2281 | +++ lib/charm-helpers/REVISION 2013-09-26 06:24:47 +0000 |
2282 | @@ -0,0 +1,1 @@ |
2283 | +76 |
2284 | |
2285 | === added file 'lib/charm-helpers/VERSION' |
2286 | --- lib/charm-helpers/VERSION 1970-01-01 00:00:00 +0000 |
2287 | +++ lib/charm-helpers/VERSION 2013-09-26 06:24:47 +0000 |
2288 | @@ -0,0 +1,1 @@ |
2289 | +0.1.2 |
2290 | |
2291 | === added directory 'lib/charm-helpers/bin' |
2292 | === added file 'lib/charm-helpers/bin/README' |
2293 | --- lib/charm-helpers/bin/README 1970-01-01 00:00:00 +0000 |
2294 | +++ lib/charm-helpers/bin/README 2013-09-26 06:24:47 +0000 |
2295 | @@ -0,0 +1,1 @@ |
2296 | +This directory contains executables for accessing charmhelpers functionality |
2297 | |
2298 | === added file 'lib/charm-helpers/bin/chlp' |
2299 | --- lib/charm-helpers/bin/chlp 1970-01-01 00:00:00 +0000 |
2300 | +++ lib/charm-helpers/bin/chlp 2013-09-26 06:24:47 +0000 |
2301 | @@ -0,0 +1,7 @@ |
2302 | +#!/usr/bin/env python |
2303 | + |
2304 | +from charmhelpers.cli import cmdline |
2305 | +from charmhelpers.cli.commands import * |
2306 | + |
2307 | +if __name__ == '__main__': |
2308 | + cmdline.run() |
2309 | |
2310 | === added directory 'lib/charm-helpers/bin/contrib' |
2311 | === added directory 'lib/charm-helpers/bin/contrib/charmsupport' |
2312 | === added file 'lib/charm-helpers/bin/contrib/charmsupport/charmsupport' |
2313 | --- lib/charm-helpers/bin/contrib/charmsupport/charmsupport 1970-01-01 00:00:00 +0000 |
2314 | +++ lib/charm-helpers/bin/contrib/charmsupport/charmsupport 2013-09-26 06:24:47 +0000 |
2315 | @@ -0,0 +1,31 @@ |
2316 | +#!/usr/bin/env python |
2317 | + |
2318 | +import argparse |
2319 | +from charmhelpers.contrib.charmsupport import execd |
2320 | + |
2321 | + |
2322 | +def run_execd(args): |
2323 | + execd.execd_run(args.module, args.dir, die_on_error=True) |
2324 | + |
2325 | + |
2326 | +def parse_args(): |
2327 | + parser = argparse.ArgumentParser(description='Perform common charm tasks') |
2328 | + subparsers = parser.add_subparsers(help='Commands') |
2329 | + |
2330 | + execd_parser = subparsers.add_parser('execd', |
2331 | + help='Execute a directory of commands') |
2332 | + execd_parser.add_argument('--module', default='charm-pre-install', |
2333 | + help='module to run (default: charm-pre-install)') |
2334 | + execd_parser.add_argument('--dir', |
2335 | + help="Override the exec.d directory path") |
2336 | + execd_parser.set_defaults(func=run_execd) |
2337 | + |
2338 | + return parser.parse_args() |
2339 | + |
2340 | + |
2341 | +def main(): |
2342 | + arguments = parse_args() |
2343 | + arguments.func(arguments) |
2344 | + |
2345 | +if __name__ == '__main__': |
2346 | + exit(main()) |
2347 | |
2348 | === added directory 'lib/charm-helpers/bin/contrib/saltstack' |
2349 | === added file 'lib/charm-helpers/bin/contrib/saltstack/salt-call' |
2350 | --- lib/charm-helpers/bin/contrib/saltstack/salt-call 1970-01-01 00:00:00 +0000 |
2351 | +++ lib/charm-helpers/bin/contrib/saltstack/salt-call 2013-09-26 06:24:47 +0000 |
2352 | @@ -0,0 +1,11 @@ |
2353 | +#!/usr/bin/env python |
2354 | +''' |
2355 | +Directly call a salt command in the modules, does not require a running salt |
2356 | +minion to run. |
2357 | +''' |
2358 | + |
2359 | +from salt.scripts import salt_call |
2360 | + |
2361 | + |
2362 | +if __name__ == '__main__': |
2363 | + salt_call() |
2364 | |
2365 | === added directory 'lib/charm-helpers/charmhelpers' |
2366 | === added file 'lib/charm-helpers/charmhelpers/__init__.py' |
2367 | === added directory 'lib/charm-helpers/charmhelpers/cli' |
2368 | === added file 'lib/charm-helpers/charmhelpers/cli/README.rst' |
2369 | --- lib/charm-helpers/charmhelpers/cli/README.rst 1970-01-01 00:00:00 +0000 |
2370 | +++ lib/charm-helpers/charmhelpers/cli/README.rst 2013-09-26 06:24:47 +0000 |
2371 | @@ -0,0 +1,57 @@ |
2372 | +========== |
2373 | +Commandant |
2374 | +========== |
2375 | + |
2376 | +----------------------------------------------------- |
2377 | +Automatic command-line interfaces to Python functions |
2378 | +----------------------------------------------------- |
2379 | + |
2380 | +One of the benefits of ``libvirt`` is the uniformity of the interface: the C API (as well as the bindings in other languages) is a set of functions that accept parameters that are nearly identical to the command-line arguments. If you run ``virsh``, you get an interactive command prompt that supports all of the same commands that your shell scripts use as ``virsh`` subcommands. |
2381 | + |
2382 | +Command execution and stdio manipulation is the greatest common factor across all development systems in the POSIX environment. By exposing your functions as commands that manipulate streams of text, you can make life easier for all the Ruby and Erlang and Go programmers in your life. |
2383 | + |
2384 | +Goals |
2385 | +===== |
2386 | + |
2387 | +* Single decorator to expose a function as a command. |
2388 | + * now two decorators - one "automatic" and one that allows authors to manipulate the arguments for fine-grained control.(MW) |
2389 | +* Automatic analysis of function signature through ``inspect.getargspec()`` |
2390 | +* Command argument parser built automatically with ``argparse`` |
2391 | +* Interactive interpreter loop object made with ``Cmd`` |
2392 | +* Options to output structured return value data via ``pprint``, ``yaml`` or ``json`` dumps. |
2393 | + |
2394 | +Other Important Features that need writing |
2395 | +------------------------------------------ |
2396 | + |
2397 | +* Help and Usage documentation can be automatically generated, but it will be important to let users override this behaviour |
2398 | +* The decorator should allow specifying further parameters to the parser's add_argument() calls, to specify types or to make arguments behave as boolean flags, etc. |
2399 | + - Filename arguments are important, as good practice is for functions to accept file objects as parameters. |
2400 | + - choices arguments help to limit bad input before the function is called |
2401 | +* Some automatic behaviour could make for better defaults, once the user can override them. |
2402 | + - We could automatically detect arguments that default to False or True, and automatically support --no-foo for foo=True. |
2403 | + - We could automatically support hyphens as alternates for underscores |
2404 | + - Arguments defaulting to sequence types could support the ``append`` action. |
2405 | + |
2406 | + |
2407 | +----------------------------------------------------- |
2408 | +Implementing subcommands |
2409 | +----------------------------------------------------- |
2410 | + |
2411 | +(WIP) |
2412 | + |
2413 | +So as to avoid dependencies on the cli module, subcommands should be defined separately from their implementations. The recommmendation would be to place definitions into separate modules near the implementations which they expose. |
2414 | + |
2415 | +Some examples:: |
2416 | + |
2417 | + from charmhelpers.cli import CommandLine |
2418 | + from charmhelpers.payload import execd |
2419 | + from charmhelpers.foo import bar |
2420 | + |
2421 | + cli = CommandLine() |
2422 | + |
2423 | + cli.subcommand(execd.execd_run) |
2424 | + |
2425 | + @cli.subcommand_builder("bar", help="Bar baz qux") |
2426 | + def barcmd_builder(subparser): |
2427 | + subparser.add_argument('argument1', help="yackety") |
2428 | + return bar |
2429 | |
2430 | === added file 'lib/charm-helpers/charmhelpers/cli/__init__.py' |
2431 | --- lib/charm-helpers/charmhelpers/cli/__init__.py 1970-01-01 00:00:00 +0000 |
2432 | +++ lib/charm-helpers/charmhelpers/cli/__init__.py 2013-09-26 06:24:47 +0000 |
2433 | @@ -0,0 +1,147 @@ |
2434 | +import inspect |
2435 | +import itertools |
2436 | +import argparse |
2437 | +import sys |
2438 | + |
2439 | + |
2440 | +class OutputFormatter(object): |
2441 | + def __init__(self, outfile=sys.stdout): |
2442 | + self.formats = ( |
2443 | + "raw", |
2444 | + "json", |
2445 | + "py", |
2446 | + "yaml", |
2447 | + "csv", |
2448 | + "tab", |
2449 | + ) |
2450 | + self.outfile = outfile |
2451 | + |
2452 | + def add_arguments(self, argument_parser): |
2453 | + formatgroup = argument_parser.add_mutually_exclusive_group() |
2454 | + choices = self.supported_formats |
2455 | + formatgroup.add_argument("--format", metavar='FMT', |
2456 | + help="Select output format for returned data, " |
2457 | + "where FMT is one of: {}".format(choices), |
2458 | + choices=choices, default='raw') |
2459 | + for fmt in self.formats: |
2460 | + fmtfunc = getattr(self, fmt) |
2461 | + formatgroup.add_argument("-{}".format(fmt[0]), |
2462 | + "--{}".format(fmt), action='store_const', |
2463 | + const=fmt, dest='format', |
2464 | + help=fmtfunc.__doc__) |
2465 | + |
2466 | + @property |
2467 | + def supported_formats(self): |
2468 | + return self.formats |
2469 | + |
2470 | + def raw(self, output): |
2471 | + """Output data as raw string (default)""" |
2472 | + self.outfile.write(str(output)) |
2473 | + |
2474 | + def py(self, output): |
2475 | + """Output data as a nicely-formatted python data structure""" |
2476 | + import pprint |
2477 | + pprint.pprint(output, stream=self.outfile) |
2478 | + |
2479 | + def json(self, output): |
2480 | + """Output data in JSON format""" |
2481 | + import json |
2482 | + json.dump(output, self.outfile) |
2483 | + |
2484 | + def yaml(self, output): |
2485 | + """Output data in YAML format""" |
2486 | + import yaml |
2487 | + yaml.safe_dump(output, self.outfile) |
2488 | + |
2489 | + def csv(self, output): |
2490 | + """Output data as excel-compatible CSV""" |
2491 | + import csv |
2492 | + csvwriter = csv.writer(self.outfile) |
2493 | + csvwriter.writerows(output) |
2494 | + |
2495 | + def tab(self, output): |
2496 | + """Output data in excel-compatible tab-delimited format""" |
2497 | + import csv |
2498 | + csvwriter = csv.writer(self.outfile, dialect=csv.excel_tab) |
2499 | + csvwriter.writerows(output) |
2500 | + |
2501 | + def format_output(self, output, fmt='raw'): |
2502 | + fmtfunc = getattr(self, fmt) |
2503 | + fmtfunc(output) |
2504 | + |
2505 | + |
2506 | +class CommandLine(object): |
2507 | + argument_parser = None |
2508 | + subparsers = None |
2509 | + formatter = None |
2510 | + |
2511 | + def __init__(self): |
2512 | + if not self.argument_parser: |
2513 | + self.argument_parser = argparse.ArgumentParser(description='Perform common charm tasks') |
2514 | + if not self.formatter: |
2515 | + self.formatter = OutputFormatter() |
2516 | + self.formatter.add_arguments(self.argument_parser) |
2517 | + if not self.subparsers: |
2518 | + self.subparsers = self.argument_parser.add_subparsers(help='Commands') |
2519 | + |
2520 | + def subcommand(self, command_name=None): |
2521 | + """ |
2522 | + Decorate a function as a subcommand. Use its arguments as the |
2523 | + command-line arguments""" |
2524 | + def wrapper(decorated): |
2525 | + cmd_name = command_name or decorated.__name__ |
2526 | + subparser = self.subparsers.add_parser(cmd_name, |
2527 | + description=decorated.__doc__) |
2528 | + for args, kwargs in describe_arguments(decorated): |
2529 | + subparser.add_argument(*args, **kwargs) |
2530 | + subparser.set_defaults(func=decorated) |
2531 | + return decorated |
2532 | + return wrapper |
2533 | + |
2534 | + def subcommand_builder(self, command_name, description=None): |
2535 | + """ |
2536 | + Decorate a function that builds a subcommand. Builders should accept a |
2537 | + single argument (the subparser instance) and return the function to be |
2538 | + run as the command.""" |
2539 | + def wrapper(decorated): |
2540 | + subparser = self.subparsers.add_parser(command_name) |
2541 | + func = decorated(subparser) |
2542 | + subparser.set_defaults(func=func) |
2543 | + subparser.description = description or func.__doc__ |
2544 | + return wrapper |
2545 | + |
2546 | + def run(self): |
2547 | + "Run cli, processing arguments and executing subcommands." |
2548 | + arguments = self.argument_parser.parse_args() |
2549 | + argspec = inspect.getargspec(arguments.func) |
2550 | + vargs = [] |
2551 | + kwargs = {} |
2552 | + if argspec.varargs: |
2553 | + vargs = getattr(arguments, argspec.varargs) |
2554 | + for arg in argspec.args: |
2555 | + kwargs[arg] = getattr(arguments, arg) |
2556 | + self.formatter.format_output(arguments.func(*vargs, **kwargs), arguments.format) |
2557 | + |
2558 | + |
2559 | +cmdline = CommandLine() |
2560 | + |
2561 | + |
2562 | +def describe_arguments(func): |
2563 | + """ |
2564 | + Analyze a function's signature and return a data structure suitable for |
2565 | + passing in as arguments to an argparse parser's add_argument() method.""" |
2566 | + |
2567 | + argspec = inspect.getargspec(func) |
2568 | + # we should probably raise an exception somewhere if func includes **kwargs |
2569 | + if argspec.defaults: |
2570 | + positional_args = argspec.args[:-len(argspec.defaults)] |
2571 | + keyword_names = argspec.args[-len(argspec.defaults):] |
2572 | + for arg, default in itertools.izip(keyword_names, argspec.defaults): |
2573 | + yield ('--{}'.format(arg),), {'default': default} |
2574 | + else: |
2575 | + positional_args = argspec.args |
2576 | + |
2577 | + for arg in positional_args: |
2578 | + yield (arg,), {} |
2579 | + if argspec.varargs: |
2580 | + yield (argspec.varargs,), {'nargs': '*'} |
2581 | |
2582 | === added file 'lib/charm-helpers/charmhelpers/cli/commands.py' |
2583 | --- lib/charm-helpers/charmhelpers/cli/commands.py 1970-01-01 00:00:00 +0000 |
2584 | +++ lib/charm-helpers/charmhelpers/cli/commands.py 2013-09-26 06:24:47 +0000 |
2585 | @@ -0,0 +1,2 @@ |
2586 | +from . import CommandLine |
2587 | +import host |
2588 | |
2589 | === added file 'lib/charm-helpers/charmhelpers/cli/host.py' |
2590 | --- lib/charm-helpers/charmhelpers/cli/host.py 1970-01-01 00:00:00 +0000 |
2591 | +++ lib/charm-helpers/charmhelpers/cli/host.py 2013-09-26 06:24:47 +0000 |
2592 | @@ -0,0 +1,14 @@ |
2593 | +from . import cmdline |
2594 | +from charmhelpers.core import host |
2595 | + |
2596 | + |
2597 | +@cmdline.subcommand() |
2598 | +def mounts(): |
2599 | + "List mounts" |
2600 | + return host.mounts() |
2601 | + |
2602 | +@cmdline.subcommand_builder('service', description="Control system services") |
2603 | +def service(subparser): |
2604 | + subparser.add_argument("action", help="The action to perform (start, stop, etc...)") |
2605 | + subparser.add_argument("service_name", help="Name of the service to control") |
2606 | + return host.service |
2607 | |
2608 | === added directory 'lib/charm-helpers/charmhelpers/contrib' |
2609 | === added file 'lib/charm-helpers/charmhelpers/contrib/__init__.py' |
2610 | === added directory 'lib/charm-helpers/charmhelpers/contrib/ansible' |
2611 | === added file 'lib/charm-helpers/charmhelpers/contrib/ansible/__init__.py' |
2612 | --- lib/charm-helpers/charmhelpers/contrib/ansible/__init__.py 1970-01-01 00:00:00 +0000 |
2613 | +++ lib/charm-helpers/charmhelpers/contrib/ansible/__init__.py 2013-09-26 06:24:47 +0000 |
2614 | @@ -0,0 +1,101 @@ |
2615 | +# Copyright 2013 Canonical Ltd. |
2616 | +# |
2617 | +# Authors: |
2618 | +# Charm Helpers Developers <juju@lists.ubuntu.com> |
2619 | +"""Charm Helpers ansible - declare the state of your machines. |
2620 | + |
2621 | +This helper enables you to declare your machine state, rather than |
2622 | +program it procedurally (and have to test each change to your procedures). |
2623 | +Your install hook can be as simple as: |
2624 | + |
2625 | +{{{ |
2626 | +import charmhelpers.contrib.ansible |
2627 | + |
2628 | + |
2629 | +def install(): |
2630 | + charmhelpers.contrib.ansible.install_ansible_support() |
2631 | + charmhelpers.contrib.ansible.apply_playbook('playbooks/install.yaml') |
2632 | +}}} |
2633 | + |
2634 | +and won't need to change (nor will its tests) when you change the machine |
2635 | +state. |
2636 | + |
2637 | +All of your juju config and relation-data are available as template |
2638 | +variables within your playbooks and templates. An install playbook looks |
2639 | +something like: |
2640 | + |
2641 | +{{{ |
2642 | +--- |
2643 | +- hosts: localhost |
2644 | + user: root |
2645 | + |
2646 | + tasks: |
2647 | + - name: Add private repositories. |
2648 | + template: |
2649 | + src: ../templates/private-repositories.list.jinja2 |
2650 | + dest: /etc/apt/sources.list.d/private.list |
2651 | + |
2652 | + - name: Update the cache. |
2653 | + apt: update_cache=yes |
2654 | + |
2655 | + - name: Install dependencies. |
2656 | + apt: pkg={{ item }} |
2657 | + with_items: |
2658 | + - python-mimeparse |
2659 | + - python-webob |
2660 | + - sunburnt |
2661 | + |
2662 | + - name: Setup groups. |
2663 | + group: name={{ item.name }} gid={{ item.gid }} |
2664 | + with_items: |
2665 | + - { name: 'deploy_user', gid: 1800 } |
2666 | + - { name: 'service_user', gid: 1500 } |
2667 | + |
2668 | + ... |
2669 | +}}} |
2670 | + |
2671 | +Read more online about playbooks[1] and standard ansible modules[2]. |
2672 | + |
2673 | +[1] http://www.ansibleworks.com/docs/playbooks.html |
2674 | +[2] http://www.ansibleworks.com/docs/modules.html |
2675 | +""" |
2676 | +import os |
2677 | +import subprocess |
2678 | + |
2679 | +import charmhelpers.contrib.saltstack |
2680 | +import charmhelpers.core.host |
2681 | +import charmhelpers.core.hookenv |
2682 | +import charmhelpers.fetch |
2683 | + |
2684 | + |
2685 | +charm_dir = os.environ.get('CHARM_DIR', '') |
2686 | +ansible_hosts_path = '/etc/ansible/hosts' |
2687 | +# Ansible will automatically include any vars in the following |
2688 | +# file in its inventory when run locally. |
2689 | +ansible_vars_path = '/etc/ansible/host_vars/localhost' |
2690 | + |
2691 | + |
2692 | +def install_ansible_support(from_ppa=True): |
2693 | + """Installs the ansible package. |
2694 | + |
2695 | + By default it is installed from the PPA [1] linked from |
2696 | + the ansible website [2]. |
2697 | + |
2698 | + [1] https://launchpad.net/~rquillo/+archive/ansible |
2699 | + [2] http://www.ansibleworks.com/docs/gettingstarted.html#ubuntu-and-debian |
2700 | + |
2701 | + If from_ppa is false, you must ensure that the package is available |
2702 | + from a configured repository. |
2703 | + """ |
2704 | + if from_ppa: |
2705 | + charmhelpers.fetch.add_source('ppa:rquillo/ansible') |
2706 | + charmhelpers.fetch.apt_update(fatal=True) |
2707 | + charmhelpers.fetch.apt_install('ansible') |
2708 | + with open(ansible_hosts_path, 'w+') as hosts_file: |
2709 | + hosts_file.write('localhost ansible_connection=local') |
2710 | + |
2711 | + |
2712 | +def apply_playbook(playbook): |
2713 | + charmhelpers.contrib.saltstack.juju_state_to_yaml( |
2714 | + ansible_vars_path, namespace_separator='__') |
2715 | + subprocess.check_call(['ansible-playbook', '-c', 'local', playbook]) |
2716 | |
2717 | === added directory 'lib/charm-helpers/charmhelpers/contrib/charmhelpers' |
2718 | === added file 'lib/charm-helpers/charmhelpers/contrib/charmhelpers/IMPORT' |
2719 | --- lib/charm-helpers/charmhelpers/contrib/charmhelpers/IMPORT 1970-01-01 00:00:00 +0000 |
2720 | +++ lib/charm-helpers/charmhelpers/contrib/charmhelpers/IMPORT 2013-09-26 06:24:47 +0000 |
2721 | @@ -0,0 +1,4 @@ |
2722 | +Source lp:charm-tools/trunk |
2723 | + |
2724 | +charm-tools/helpers/python/charmhelpers/__init__.py -> charmhelpers/charmhelpers/contrib/charmhelpers/__init__.py |
2725 | +charm-tools/helpers/python/charmhelpers/tests/test_charmhelpers.py -> charmhelpers/tests/contrib/charmhelpers/test_charmhelpers.py |
2726 | |
2727 | === added file 'lib/charm-helpers/charmhelpers/contrib/charmhelpers/__init__.py' |
2728 | --- lib/charm-helpers/charmhelpers/contrib/charmhelpers/__init__.py 1970-01-01 00:00:00 +0000 |
2729 | +++ lib/charm-helpers/charmhelpers/contrib/charmhelpers/__init__.py 2013-09-26 06:24:47 +0000 |
2730 | @@ -0,0 +1,184 @@ |
2731 | +# Copyright 2012 Canonical Ltd. This software is licensed under the |
2732 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
2733 | + |
2734 | +import warnings |
2735 | +warnings.warn("contrib.charmhelpers is deprecated", DeprecationWarning) |
2736 | + |
2737 | +"""Helper functions for writing Juju charms in Python.""" |
2738 | + |
2739 | +__metaclass__ = type |
2740 | +__all__ = [ |
2741 | + #'get_config', # core.hookenv.config() |
2742 | + #'log', # core.hookenv.log() |
2743 | + #'log_entry', # core.hookenv.log() |
2744 | + #'log_exit', # core.hookenv.log() |
2745 | + #'relation_get', # core.hookenv.relation_get() |
2746 | + #'relation_set', # core.hookenv.relation_set() |
2747 | + #'relation_ids', # core.hookenv.relation_ids() |
2748 | + #'relation_list', # core.hookenv.relation_units() |
2749 | + #'config_get', # core.hookenv.config() |
2750 | + #'unit_get', # core.hookenv.unit_get() |
2751 | + #'open_port', # core.hookenv.open_port() |
2752 | + #'close_port', # core.hookenv.close_port() |
2753 | + #'service_control', # core.host.service() |
2754 | + 'unit_info', # client-side, NOT IMPLEMENTED |
2755 | + 'wait_for_machine', # client-side, NOT IMPLEMENTED |
2756 | + 'wait_for_page_contents', # client-side, NOT IMPLEMENTED |
2757 | + 'wait_for_relation', # client-side, NOT IMPLEMENTED |
2758 | + 'wait_for_unit', # client-side, NOT IMPLEMENTED |
2759 | +] |
2760 | + |
2761 | +import operator |
2762 | +from shelltoolbox import ( |
2763 | + command, |
2764 | +) |
2765 | +import tempfile |
2766 | +import time |
2767 | +import urllib2 |
2768 | +import yaml |
2769 | + |
2770 | +SLEEP_AMOUNT = 0.1 |
2771 | +# We create a juju_status Command here because it makes testing much, |
2772 | +# much easier. |
2773 | +juju_status = lambda: command('juju')('status') |
2774 | + |
2775 | +# re-implemented as charmhelpers.fetch.configure_sources() |
2776 | +#def configure_source(update=False): |
2777 | +# source = config_get('source') |
2778 | +# if ((source.startswith('ppa:') or |
2779 | +# source.startswith('cloud:') or |
2780 | +# source.startswith('http:'))): |
2781 | +# run('add-apt-repository', source) |
2782 | +# if source.startswith("http:"): |
2783 | +# run('apt-key', 'import', config_get('key')) |
2784 | +# if update: |
2785 | +# run('apt-get', 'update') |
2786 | + |
2787 | + |
2788 | +# DEPRECATED: client-side only |
2789 | +def make_charm_config_file(charm_config): |
2790 | + charm_config_file = tempfile.NamedTemporaryFile() |
2791 | + charm_config_file.write(yaml.dump(charm_config)) |
2792 | + charm_config_file.flush() |
2793 | + # The NamedTemporaryFile instance is returned instead of just the name |
2794 | + # because we want to take advantage of garbage collection-triggered |
2795 | + # deletion of the temp file when it goes out of scope in the caller. |
2796 | + return charm_config_file |
2797 | + |
2798 | + |
2799 | +# DEPRECATED: client-side only |
2800 | +def unit_info(service_name, item_name, data=None, unit=None): |
2801 | + if data is None: |
2802 | + data = yaml.safe_load(juju_status()) |
2803 | + service = data['services'].get(service_name) |
2804 | + if service is None: |
2805 | + # XXX 2012-02-08 gmb: |
2806 | + # This allows us to cope with the race condition that we |
2807 | + # have between deploying a service and having it come up in |
2808 | + # `juju status`. We could probably do with cleaning it up so |
2809 | + # that it fails a bit more noisily after a while. |
2810 | + return '' |
2811 | + units = service['units'] |
2812 | + if unit is not None: |
2813 | + item = units[unit][item_name] |
2814 | + else: |
2815 | + # It might seem odd to sort the units here, but we do it to |
2816 | + # ensure that when no unit is specified, the first unit for the |
2817 | + # service (or at least the one with the lowest number) is the |
2818 | + # one whose data gets returned. |
2819 | + sorted_unit_names = sorted(units.keys()) |
2820 | + item = units[sorted_unit_names[0]][item_name] |
2821 | + return item |
2822 | + |
2823 | + |
2824 | +# DEPRECATED: client-side only |
2825 | +def get_machine_data(): |
2826 | + return yaml.safe_load(juju_status())['machines'] |
2827 | + |
2828 | + |
2829 | +# DEPRECATED: client-side only |
2830 | +def wait_for_machine(num_machines=1, timeout=300): |
2831 | + """Wait `timeout` seconds for `num_machines` machines to come up. |
2832 | + |
2833 | + This wait_for... function can be called by other wait_for functions |
2834 | + whose timeouts might be too short in situations where only a bare |
2835 | + Juju setup has been bootstrapped. |
2836 | + |
2837 | + :return: A tuple of (num_machines, time_taken). This is used for |
2838 | + testing. |
2839 | + """ |
2840 | + # You may think this is a hack, and you'd be right. The easiest way |
2841 | + # to tell what environment we're working in (LXC vs EC2) is to check |
2842 | + # the dns-name of the first machine. If it's localhost we're in LXC |
2843 | + # and we can just return here. |
2844 | + if get_machine_data()[0]['dns-name'] == 'localhost': |
2845 | + return 1, 0 |
2846 | + start_time = time.time() |
2847 | + while True: |
2848 | + # Drop the first machine, since it's the Zookeeper and that's |
2849 | + # not a machine that we need to wait for. This will only work |
2850 | + # for EC2 environments, which is why we return early above if |
2851 | + # we're in LXC. |
2852 | + machine_data = get_machine_data() |
2853 | + non_zookeeper_machines = [ |
2854 | + machine_data[key] for key in machine_data.keys()[1:]] |
2855 | + if len(non_zookeeper_machines) >= num_machines: |
2856 | + all_machines_running = True |
2857 | + for machine in non_zookeeper_machines: |
2858 | + if machine.get('instance-state') != 'running': |
2859 | + all_machines_running = False |
2860 | + break |
2861 | + if all_machines_running: |
2862 | + break |
2863 | + if time.time() - start_time >= timeout: |
2864 | + raise RuntimeError('timeout waiting for service to start') |
2865 | + time.sleep(SLEEP_AMOUNT) |
2866 | + return num_machines, time.time() - start_time |
2867 | + |
2868 | + |
2869 | +# DEPRECATED: client-side only |
2870 | +def wait_for_unit(service_name, timeout=480): |
2871 | + """Wait `timeout` seconds for a given service name to come up.""" |
2872 | + wait_for_machine(num_machines=1) |
2873 | + start_time = time.time() |
2874 | + while True: |
2875 | + state = unit_info(service_name, 'agent-state') |
2876 | + if 'error' in state or state == 'started': |
2877 | + break |
2878 | + if time.time() - start_time >= timeout: |
2879 | + raise RuntimeError('timeout waiting for service to start') |
2880 | + time.sleep(SLEEP_AMOUNT) |
2881 | + if state != 'started': |
2882 | + raise RuntimeError('unit did not start, agent-state: ' + state) |
2883 | + |
2884 | + |
2885 | +# DEPRECATED: client-side only |
2886 | +def wait_for_relation(service_name, relation_name, timeout=120): |
2887 | + """Wait `timeout` seconds for a given relation to come up.""" |
2888 | + start_time = time.time() |
2889 | + while True: |
2890 | + relation = unit_info(service_name, 'relations').get(relation_name) |
2891 | + if relation is not None and relation['state'] == 'up': |
2892 | + break |
2893 | + if time.time() - start_time >= timeout: |
2894 | + raise RuntimeError('timeout waiting for relation to be up') |
2895 | + time.sleep(SLEEP_AMOUNT) |
2896 | + |
2897 | + |
2898 | +# DEPRECATED: client-side only |
2899 | +def wait_for_page_contents(url, contents, timeout=120, validate=None): |
2900 | + if validate is None: |
2901 | + validate = operator.contains |
2902 | + start_time = time.time() |
2903 | + while True: |
2904 | + try: |
2905 | + stream = urllib2.urlopen(url) |
2906 | + except (urllib2.HTTPError, urllib2.URLError): |
2907 | + pass |
2908 | + else: |
2909 | + page = stream.read() |
2910 | + if validate(page, contents): |
2911 | + return page |
2912 | + if time.time() - start_time >= timeout: |
2913 | + raise RuntimeError('timeout waiting for contents of ' + url) |
2914 | + time.sleep(SLEEP_AMOUNT) |
2915 | |
2916 | === added directory 'lib/charm-helpers/charmhelpers/contrib/charmsupport' |
2917 | === added file 'lib/charm-helpers/charmhelpers/contrib/charmsupport/IMPORT' |
2918 | --- lib/charm-helpers/charmhelpers/contrib/charmsupport/IMPORT 1970-01-01 00:00:00 +0000 |
2919 | +++ lib/charm-helpers/charmhelpers/contrib/charmsupport/IMPORT 2013-09-26 06:24:47 +0000 |
2920 | @@ -0,0 +1,14 @@ |
2921 | +Source: lp:charmsupport/trunk |
2922 | + |
2923 | +charmsupport/charmsupport/execd.py -> charm-helpers/charmhelpers/contrib/charmsupport/execd.py |
2924 | +charmsupport/charmsupport/hookenv.py -> charm-helpers/charmhelpers/contrib/charmsupport/hookenv.py |
2925 | +charmsupport/charmsupport/host.py -> charm-helpers/charmhelpers/contrib/charmsupport/host.py |
2926 | +charmsupport/charmsupport/nrpe.py -> charm-helpers/charmhelpers/contrib/charmsupport/nrpe.py |
2927 | +charmsupport/charmsupport/volumes.py -> charm-helpers/charmhelpers/contrib/charmsupport/volumes.py |
2928 | + |
2929 | +charmsupport/tests/test_execd.py -> charm-helpers/tests/contrib/charmsupport/test_execd.py |
2930 | +charmsupport/tests/test_hookenv.py -> charm-helpers/tests/contrib/charmsupport/test_hookenv.py |
2931 | +charmsupport/tests/test_host.py -> charm-helpers/tests/contrib/charmsupport/test_host.py |
2932 | +charmsupport/tests/test_nrpe.py -> charm-helpers/tests/contrib/charmsupport/test_nrpe.py |
2933 | + |
2934 | +charmsupport/bin/charmsupport -> charm-helpers/bin/contrib/charmsupport/charmsupport |
2935 | |
2936 | === added file 'lib/charm-helpers/charmhelpers/contrib/charmsupport/__init__.py' |
2937 | === added file 'lib/charm-helpers/charmhelpers/contrib/charmsupport/nrpe.py' |
2938 | --- lib/charm-helpers/charmhelpers/contrib/charmsupport/nrpe.py 1970-01-01 00:00:00 +0000 |
2939 | +++ lib/charm-helpers/charmhelpers/contrib/charmsupport/nrpe.py 2013-09-26 06:24:47 +0000 |
2940 | @@ -0,0 +1,218 @@ |
2941 | +"""Compatibility with the nrpe-external-master charm""" |
2942 | +# Copyright 2012 Canonical Ltd. |
2943 | +# |
2944 | +# Authors: |
2945 | +# Matthew Wedgwood <matthew.wedgwood@canonical.com> |
2946 | + |
2947 | +import subprocess |
2948 | +import pwd |
2949 | +import grp |
2950 | +import os |
2951 | +import re |
2952 | +import shlex |
2953 | +import yaml |
2954 | + |
2955 | +from charmhelpers.core.hookenv import ( |
2956 | + config, |
2957 | + local_unit, |
2958 | + log, |
2959 | + relation_ids, |
2960 | + relation_set, |
2961 | +) |
2962 | + |
2963 | +from charmhelpers.core.host import service |
2964 | + |
2965 | +# This module adds compatibility with the nrpe-external-master and plain nrpe |
2966 | +# subordinate charms. To use it in your charm: |
2967 | +# |
2968 | +# 1. Update metadata.yaml |
2969 | +# |
2970 | +# provides: |
2971 | +# (...) |
2972 | +# nrpe-external-master: |
2973 | +# interface: nrpe-external-master |
2974 | +# scope: container |
2975 | +# |
2976 | +# and/or |
2977 | +# |
2978 | +# provides: |
2979 | +# (...) |
2980 | +# local-monitors: |
2981 | +# interface: local-monitors |
2982 | +# scope: container |
2983 | + |
2984 | +# |
2985 | +# 2. Add the following to config.yaml |
2986 | +# |
2987 | +# nagios_context: |
2988 | +# default: "juju" |
2989 | +# type: string |
2990 | +# description: | |
2991 | +# Used by the nrpe subordinate charms. |
2992 | +# A string that will be prepended to instance name to set the host name |
2993 | +# in nagios. So for instance the hostname would be something like: |
2994 | +# juju-myservice-0 |
2995 | +# If you're running multiple environments with the same services in them |
2996 | +# this allows you to differentiate between them. |
2997 | +# |
2998 | +# 3. Add custom checks (Nagios plugins) to files/nrpe-external-master |
2999 | +# |
3000 | +# 4. Update your hooks.py with something like this: |
3001 | +# |
3002 | +# from charmsupport.nrpe import NRPE |
3003 | +# (...) |
3004 | +# def update_nrpe_config(): |
3005 | +# nrpe_compat = NRPE() |
3006 | +# nrpe_compat.add_check( |
3007 | +# shortname = "myservice", |
3008 | +# description = "Check MyService", |
3009 | +# check_cmd = "check_http -w 2 -c 10 http://localhost" |
3010 | +# ) |
3011 | +# nrpe_compat.add_check( |
3012 | +# "myservice_other", |
3013 | +# "Check for widget failures", |
3014 | +# check_cmd = "/srv/myapp/scripts/widget_check" |
3015 | +# ) |
3016 | +# nrpe_compat.write() |
3017 | +# |
3018 | +# def config_changed(): |
3019 | +# (...) |
3020 | +# update_nrpe_config() |
3021 | +# |
3022 | +# def nrpe_external_master_relation_changed(): |
3023 | +# update_nrpe_config() |
3024 | +# |
3025 | +# def local_monitors_relation_changed(): |
3026 | +# update_nrpe_config() |
3027 | +# |
3028 | +# 5. ln -s hooks.py nrpe-external-master-relation-changed |
3029 | +# ln -s hooks.py local-monitors-relation-changed |
3030 | + |
3031 | + |
3032 | +class CheckException(Exception): |
3033 | + pass |
3034 | + |
3035 | + |
3036 | +class Check(object): |
3037 | + shortname_re = '[A-Za-z0-9-_]+$' |
3038 | + service_template = (""" |
3039 | +#--------------------------------------------------- |
3040 | +# This file is Juju managed |
3041 | +#--------------------------------------------------- |
3042 | +define service {{ |
3043 | + use active-service |
3044 | + host_name {nagios_hostname} |
3045 | + service_description {nagios_hostname}[{shortname}] """ |
3046 | + """{description} |
3047 | + check_command check_nrpe!{command} |
3048 | + servicegroups {nagios_servicegroup} |
3049 | +}} |
3050 | +""") |
3051 | + |
3052 | + def __init__(self, shortname, description, check_cmd): |
3053 | + super(Check, self).__init__() |
3054 | + # XXX: could be better to calculate this from the service name |
3055 | + if not re.match(self.shortname_re, shortname): |
3056 | + raise CheckException("shortname must match {}".format( |
3057 | + Check.shortname_re)) |
3058 | + self.shortname = shortname |
3059 | + self.command = "check_{}".format(shortname) |
3060 | + # Note: a set of invalid characters is defined by the |
3061 | + # Nagios server config |
3062 | + # The default is: illegal_object_name_chars=`~!$%^&*"|'<>?,()= |
3063 | + self.description = description |
3064 | + self.check_cmd = self._locate_cmd(check_cmd) |
3065 | + |
3066 | + def _locate_cmd(self, check_cmd): |
3067 | + search_path = ( |
3068 | + '/', |
3069 | + os.path.join(os.environ['CHARM_DIR'], |
3070 | + 'files/nrpe-external-master'), |
3071 | + '/usr/lib/nagios/plugins', |
3072 | + ) |
3073 | + parts = shlex.split(check_cmd) |
3074 | + for path in search_path: |
3075 | + if os.path.exists(os.path.join(path, parts[0])): |
3076 | + command = os.path.join(path, parts[0]) |
3077 | + if len(parts) > 1: |
3078 | + command += " " + " ".join(parts[1:]) |
3079 | + return command |
3080 | + log('Check command not found: {}'.format(parts[0])) |
3081 | + return '' |
3082 | + |
3083 | + def write(self, nagios_context, hostname): |
3084 | + nrpe_check_file = '/etc/nagios/nrpe.d/{}.cfg'.format( |
3085 | + self.command) |
3086 | + with open(nrpe_check_file, 'w') as nrpe_check_config: |
3087 | + nrpe_check_config.write("# check {}\n".format(self.shortname)) |
3088 | + nrpe_check_config.write("command[{}]={}\n".format( |
3089 | + self.command, self.check_cmd)) |
3090 | + |
3091 | + if not os.path.exists(NRPE.nagios_exportdir): |
3092 | + log('Not writing service config as {} is not accessible'.format( |
3093 | + NRPE.nagios_exportdir)) |
3094 | + else: |
3095 | + self.write_service_config(nagios_context, hostname) |
3096 | + |
3097 | + def write_service_config(self, nagios_context, hostname): |
3098 | + for f in os.listdir(NRPE.nagios_exportdir): |
3099 | + if re.search('.*{}.cfg'.format(self.command), f): |
3100 | + os.remove(os.path.join(NRPE.nagios_exportdir, f)) |
3101 | + |
3102 | + templ_vars = { |
3103 | + 'nagios_hostname': hostname, |
3104 | + 'nagios_servicegroup': nagios_context, |
3105 | + 'description': self.description, |
3106 | + 'shortname': self.shortname, |
3107 | + 'command': self.command, |
3108 | + } |
3109 | + nrpe_service_text = Check.service_template.format(**templ_vars) |
3110 | + nrpe_service_file = '{}/service__{}_{}.cfg'.format( |
3111 | + NRPE.nagios_exportdir, hostname, self.command) |
3112 | + with open(nrpe_service_file, 'w') as nrpe_service_config: |
3113 | + nrpe_service_config.write(str(nrpe_service_text)) |
3114 | + |
3115 | + def run(self): |
3116 | + subprocess.call(self.check_cmd) |
3117 | + |
3118 | + |
3119 | +class NRPE(object): |
3120 | + nagios_logdir = '/var/log/nagios' |
3121 | + nagios_exportdir = '/var/lib/nagios/export' |
3122 | + nrpe_confdir = '/etc/nagios/nrpe.d' |
3123 | + |
3124 | + def __init__(self): |
3125 | + super(NRPE, self).__init__() |
3126 | + self.config = config() |
3127 | + self.nagios_context = self.config['nagios_context'] |
3128 | + self.unit_name = local_unit().replace('/', '-') |
3129 | + self.hostname = "{}-{}".format(self.nagios_context, self.unit_name) |
3130 | + self.checks = [] |
3131 | + |
3132 | + def add_check(self, *args, **kwargs): |
3133 | + self.checks.append(Check(*args, **kwargs)) |
3134 | + |
3135 | + def write(self): |
3136 | + try: |
3137 | + nagios_uid = pwd.getpwnam('nagios').pw_uid |
3138 | + nagios_gid = grp.getgrnam('nagios').gr_gid |
3139 | + except: |
3140 | + log("Nagios user not set up, nrpe checks not updated") |
3141 | + return |
3142 | + |
3143 | + if not os.path.exists(NRPE.nagios_logdir): |
3144 | + os.mkdir(NRPE.nagios_logdir) |
3145 | + os.chown(NRPE.nagios_logdir, nagios_uid, nagios_gid) |
3146 | + |
3147 | + nrpe_monitors = {} |
3148 | + monitors = {"monitors": {"remote": {"nrpe": nrpe_monitors}}} |
3149 | + for nrpecheck in self.checks: |
3150 | + nrpecheck.write(self.nagios_context, self.hostname) |
3151 | + nrpe_monitors[nrpecheck.shortname] = { |
3152 | + "command": nrpecheck.command, |
3153 | + } |
3154 | + |
3155 | + service('restart', 'nagios-nrpe-server') |
3156 | + |
3157 | + for rid in relation_ids("local-monitors"): |
3158 | + relation_set(relation_id=rid, monitors=yaml.dump(monitors)) |
3159 | |
3160 | === added file 'lib/charm-helpers/charmhelpers/contrib/charmsupport/volumes.py' |
3161 | --- lib/charm-helpers/charmhelpers/contrib/charmsupport/volumes.py 1970-01-01 00:00:00 +0000 |
3162 | +++ lib/charm-helpers/charmhelpers/contrib/charmsupport/volumes.py 2013-09-26 06:24:47 +0000 |
3163 | @@ -0,0 +1,156 @@ |
3164 | +''' |
3165 | +Functions for managing volumes in juju units. One volume is supported per unit. |
3166 | +Subordinates may have their own storage, provided it is on its own partition. |
3167 | + |
3168 | +Configuration stanzas: |
3169 | + volume-ephemeral: |
3170 | + type: boolean |
3171 | + default: true |
3172 | + description: > |
3173 | + If false, a volume is mounted as sepecified in "volume-map" |
3174 | + If true, ephemeral storage will be used, meaning that log data |
3175 | + will only exist as long as the machine. YOU HAVE BEEN WARNED. |
3176 | + volume-map: |
3177 | + type: string |
3178 | + default: {} |
3179 | + description: > |
3180 | + YAML map of units to device names, e.g: |
3181 | + "{ rsyslog/0: /dev/vdb, rsyslog/1: /dev/vdb }" |
3182 | + Service units will raise a configure-error if volume-ephemeral |
3183 | + is 'true' and no volume-map value is set. Use 'juju set' to set a |
3184 | + value and 'juju resolved' to complete configuration. |
3185 | + |
3186 | +Usage: |
3187 | + from charmsupport.volumes import configure_volume, VolumeConfigurationError |
3188 | + from charmsupport.hookenv import log, ERROR |
3189 | + def post_mount_hook(): |
3190 | + stop_service('myservice') |
3191 | + def post_mount_hook(): |
3192 | + start_service('myservice') |
3193 | + |
3194 | + if __name__ == '__main__': |
3195 | + try: |
3196 | + configure_volume(before_change=pre_mount_hook, |
3197 | + after_change=post_mount_hook) |
3198 | + except VolumeConfigurationError: |
3199 | + log('Storage could not be configured', ERROR) |
3200 | +''' |
3201 | + |
3202 | +# XXX: Known limitations |
3203 | +# - fstab is neither consulted nor updated |
3204 | + |
3205 | +import os |
3206 | +from charmhelpers.core import hookenv |
3207 | +from charmhelpers.core import host |
3208 | +import yaml |
3209 | + |
3210 | + |
3211 | +MOUNT_BASE = '/srv/juju/volumes' |
3212 | + |
3213 | + |
3214 | +class VolumeConfigurationError(Exception): |
3215 | + '''Volume configuration data is missing or invalid''' |
3216 | + pass |
3217 | + |
3218 | + |
3219 | +def get_config(): |
3220 | + '''Gather and sanity-check volume configuration data''' |
3221 | + volume_config = {} |
3222 | + config = hookenv.config() |
3223 | + |
3224 | + errors = False |
3225 | + |
3226 | + if config.get('volume-ephemeral') in (True, 'True', 'true', 'Yes', 'yes'): |
3227 | + volume_config['ephemeral'] = True |
3228 | + else: |
3229 | + volume_config['ephemeral'] = False |
3230 | + |
3231 | + try: |
3232 | + volume_map = yaml.safe_load(config.get('volume-map', '{}')) |
3233 | + except yaml.YAMLError as e: |
3234 | + hookenv.log("Error parsing YAML volume-map: {}".format(e), |
3235 | + hookenv.ERROR) |
3236 | + errors = True |
3237 | + if volume_map is None: |
3238 | + # probably an empty string |
3239 | + volume_map = {} |
3240 | + elif not isinstance(volume_map, dict): |
3241 | + hookenv.log("Volume-map should be a dictionary, not {}".format( |
3242 | + type(volume_map))) |
3243 | + errors = True |
3244 | + |
3245 | + volume_config['device'] = volume_map.get(os.environ['JUJU_UNIT_NAME']) |
3246 | + if volume_config['device'] and volume_config['ephemeral']: |
3247 | + # asked for ephemeral storage but also defined a volume ID |
3248 | + hookenv.log('A volume is defined for this unit, but ephemeral ' |
3249 | + 'storage was requested', hookenv.ERROR) |
3250 | + errors = True |
3251 | + elif not volume_config['device'] and not volume_config['ephemeral']: |
3252 | + # asked for permanent storage but did not define volume ID |
3253 | + hookenv.log('Ephemeral storage was requested, but there is no volume ' |
3254 | + 'defined for this unit.', hookenv.ERROR) |
3255 | + errors = True |
3256 | + |
3257 | + unit_mount_name = hookenv.local_unit().replace('/', '-') |
3258 | + volume_config['mountpoint'] = os.path.join(MOUNT_BASE, unit_mount_name) |
3259 | + |
3260 | + if errors: |
3261 | + return None |
3262 | + return volume_config |
3263 | + |
3264 | + |
3265 | +def mount_volume(config): |
3266 | + if os.path.exists(config['mountpoint']): |
3267 | + if not os.path.isdir(config['mountpoint']): |
3268 | + hookenv.log('Not a directory: {}'.format(config['mountpoint'])) |
3269 | + raise VolumeConfigurationError() |
3270 | + else: |
3271 | + host.mkdir(config['mountpoint']) |
3272 | + if os.path.ismount(config['mountpoint']): |
3273 | + unmount_volume(config) |
3274 | + if not host.mount(config['device'], config['mountpoint'], persist=True): |
3275 | + raise VolumeConfigurationError() |
3276 | + |
3277 | + |
3278 | +def unmount_volume(config): |
3279 | + if os.path.ismount(config['mountpoint']): |
3280 | + if not host.umount(config['mountpoint'], persist=True): |
3281 | + raise VolumeConfigurationError() |
3282 | + |
3283 | + |
3284 | +def managed_mounts(): |
3285 | + '''List of all mounted managed volumes''' |
3286 | + return filter(lambda mount: mount[0].startswith(MOUNT_BASE), host.mounts()) |
3287 | + |
3288 | + |
3289 | +def configure_volume(before_change=lambda: None, after_change=lambda: None): |
3290 | + '''Set up storage (or don't) according to the charm's volume configuration. |
3291 | + Returns the mount point or "ephemeral". before_change and after_change |
3292 | + are optional functions to be called if the volume configuration changes. |
3293 | + ''' |
3294 | + |
3295 | + config = get_config() |
3296 | + if not config: |
3297 | + hookenv.log('Failed to read volume configuration', hookenv.CRITICAL) |
3298 | + raise VolumeConfigurationError() |
3299 | + |
3300 | + if config['ephemeral']: |
3301 | + if os.path.ismount(config['mountpoint']): |
3302 | + before_change() |
3303 | + unmount_volume(config) |
3304 | + after_change() |
3305 | + return 'ephemeral' |
3306 | + else: |
3307 | + # persistent storage |
3308 | + if os.path.ismount(config['mountpoint']): |
3309 | + mounts = dict(managed_mounts()) |
3310 | + if mounts.get(config['mountpoint']) != config['device']: |
3311 | + before_change() |
3312 | + unmount_volume(config) |
3313 | + mount_volume(config) |
3314 | + after_change() |
3315 | + else: |
3316 | + before_change() |
3317 | + mount_volume(config) |
3318 | + after_change() |
3319 | + return config['mountpoint'] |
3320 | |
3321 | === added directory 'lib/charm-helpers/charmhelpers/contrib/hahelpers' |
3322 | === added file 'lib/charm-helpers/charmhelpers/contrib/hahelpers/__init__.py' |
3323 | === added file 'lib/charm-helpers/charmhelpers/contrib/hahelpers/apache.py' |
3324 | --- lib/charm-helpers/charmhelpers/contrib/hahelpers/apache.py 1970-01-01 00:00:00 +0000 |
3325 | +++ lib/charm-helpers/charmhelpers/contrib/hahelpers/apache.py 2013-09-26 06:24:47 +0000 |
3326 | @@ -0,0 +1,58 @@ |
3327 | +# |
3328 | +# Copyright 2012 Canonical Ltd. |
3329 | +# |
3330 | +# This file is sourced from lp:openstack-charm-helpers |
3331 | +# |
3332 | +# Authors: |
3333 | +# James Page <james.page@ubuntu.com> |
3334 | +# Adam Gandelman <adamg@ubuntu.com> |
3335 | +# |
3336 | + |
3337 | +import subprocess |
3338 | + |
3339 | +from charmhelpers.core.hookenv import ( |
3340 | + config as config_get, |
3341 | + relation_get, |
3342 | + relation_ids, |
3343 | + related_units as relation_list, |
3344 | + log, |
3345 | + INFO, |
3346 | +) |
3347 | + |
3348 | + |
3349 | +def get_cert(): |
3350 | + cert = config_get('ssl_cert') |
3351 | + key = config_get('ssl_key') |
3352 | + if not (cert and key): |
3353 | + log("Inspecting identity-service relations for SSL certificate.", |
3354 | + level=INFO) |
3355 | + cert = key = None |
3356 | + for r_id in relation_ids('identity-service'): |
3357 | + for unit in relation_list(r_id): |
3358 | + if not cert: |
3359 | + cert = relation_get('ssl_cert', |
3360 | + rid=r_id, unit=unit) |
3361 | + if not key: |
3362 | + key = relation_get('ssl_key', |
3363 | + rid=r_id, unit=unit) |
3364 | + return (cert, key) |
3365 | + |
3366 | + |
3367 | +def get_ca_cert(): |
3368 | + ca_cert = None |
3369 | + log("Inspecting identity-service relations for CA SSL certificate.", |
3370 | + level=INFO) |
3371 | + for r_id in relation_ids('identity-service'): |
3372 | + for unit in relation_list(r_id): |
3373 | + if not ca_cert: |
3374 | + ca_cert = relation_get('ca_cert', |
3375 | + rid=r_id, unit=unit) |
3376 | + return ca_cert |
3377 | + |
3378 | + |
3379 | +def install_ca_cert(ca_cert): |
3380 | + if ca_cert: |
3381 | + with open('/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt', |
3382 | + 'w') as crt: |
3383 | + crt.write(ca_cert) |
3384 | + subprocess.check_call(['update-ca-certificates', '--fresh']) |
3385 | |
3386 | === added file 'lib/charm-helpers/charmhelpers/contrib/hahelpers/ceph.py' |
3387 | --- lib/charm-helpers/charmhelpers/contrib/hahelpers/ceph.py 1970-01-01 00:00:00 +0000 |
3388 | +++ lib/charm-helpers/charmhelpers/contrib/hahelpers/ceph.py 2013-09-26 06:24:47 +0000 |
3389 | @@ -0,0 +1,294 @@ |
3390 | +# |
3391 | +# Copyright 2012 Canonical Ltd. |
3392 | +# |
3393 | +# This file is sourced from lp:openstack-charm-helpers |
3394 | +# |
3395 | +# Authors: |
3396 | +# James Page <james.page@ubuntu.com> |
3397 | +# Adam Gandelman <adamg@ubuntu.com> |
3398 | +# |
3399 | + |
3400 | +import commands |
3401 | +import os |
3402 | +import shutil |
3403 | +import time |
3404 | + |
3405 | +from subprocess import ( |
3406 | + check_call, |
3407 | + check_output, |
3408 | + CalledProcessError |
3409 | +) |
3410 | + |
3411 | +from charmhelpers.core.hookenv import ( |
3412 | + relation_get, |
3413 | + relation_ids, |
3414 | + related_units, |
3415 | + log, |
3416 | + INFO, |
3417 | + ERROR |
3418 | +) |
3419 | + |
3420 | +from charmhelpers.fetch import ( |
3421 | + apt_install, |
3422 | +) |
3423 | + |
3424 | +from charmhelpers.core.host import ( |
3425 | + mount, |
3426 | + mounts, |
3427 | + service_start, |
3428 | + service_stop, |
3429 | + umount, |
3430 | +) |
3431 | + |
3432 | +KEYRING = '/etc/ceph/ceph.client.%s.keyring' |
3433 | +KEYFILE = '/etc/ceph/ceph.client.%s.key' |
3434 | + |
3435 | +CEPH_CONF = """[global] |
3436 | + auth supported = %(auth)s |
3437 | + keyring = %(keyring)s |
3438 | + mon host = %(mon_hosts)s |
3439 | +""" |
3440 | + |
3441 | + |
3442 | +def running(service): |
3443 | + # this local util can be dropped as soon the following branch lands |
3444 | + # in lp:charm-helpers |
3445 | + # https://code.launchpad.net/~gandelman-a/charm-helpers/service_running/ |
3446 | + try: |
3447 | + output = check_output(['service', service, 'status']) |
3448 | + except CalledProcessError: |
3449 | + return False |
3450 | + else: |
3451 | + if ("start/running" in output or "is running" in output): |
3452 | + return True |
3453 | + else: |
3454 | + return False |
3455 | + |
3456 | + |
3457 | +def install(): |
3458 | + ceph_dir = "/etc/ceph" |
3459 | + if not os.path.isdir(ceph_dir): |
3460 | + os.mkdir(ceph_dir) |
3461 | + apt_install('ceph-common', fatal=True) |
3462 | + |
3463 | + |
3464 | +def rbd_exists(service, pool, rbd_img): |
3465 | + (rc, out) = commands.getstatusoutput('rbd list --id %s --pool %s' % |
3466 | + (service, pool)) |
3467 | + return rbd_img in out |
3468 | + |
3469 | + |
3470 | +def create_rbd_image(service, pool, image, sizemb): |
3471 | + cmd = [ |
3472 | + 'rbd', |
3473 | + 'create', |
3474 | + image, |
3475 | + '--size', |
3476 | + str(sizemb), |
3477 | + '--id', |
3478 | + service, |
3479 | + '--pool', |
3480 | + pool |
3481 | + ] |
3482 | + check_call(cmd) |
3483 | + |
3484 | + |
3485 | +def pool_exists(service, name): |
3486 | + (rc, out) = commands.getstatusoutput("rados --id %s lspools" % service) |
3487 | + return name in out |
3488 | + |
3489 | + |
3490 | +def create_pool(service, name): |
3491 | + cmd = [ |
3492 | + 'rados', |
3493 | + '--id', |
3494 | + service, |
3495 | + 'mkpool', |
3496 | + name |
3497 | + ] |
3498 | + check_call(cmd) |
3499 | + |
3500 | + |
3501 | +def keyfile_path(service): |
3502 | + return KEYFILE % service |
3503 | + |
3504 | + |
3505 | +def keyring_path(service): |
3506 | + return KEYRING % service |
3507 | + |
3508 | + |
3509 | +def create_keyring(service, key): |
3510 | + keyring = keyring_path(service) |
3511 | + if os.path.exists(keyring): |
3512 | + log('ceph: Keyring exists at %s.' % keyring, level=INFO) |
3513 | + cmd = [ |
3514 | + 'ceph-authtool', |
3515 | + keyring, |
3516 | + '--create-keyring', |
3517 | + '--name=client.%s' % service, |
3518 | + '--add-key=%s' % key |
3519 | + ] |
3520 | + check_call(cmd) |
3521 | + log('ceph: Created new ring at %s.' % keyring, level=INFO) |
3522 | + |
3523 | + |
3524 | +def create_key_file(service, key): |
3525 | + # create a file containing the key |
3526 | + keyfile = keyfile_path(service) |
3527 | + if os.path.exists(keyfile): |
3528 | + log('ceph: Keyfile exists at %s.' % keyfile, level=INFO) |
3529 | + fd = open(keyfile, 'w') |
3530 | + fd.write(key) |
3531 | + fd.close() |
3532 | + log('ceph: Created new keyfile at %s.' % keyfile, level=INFO) |
3533 | + |
3534 | + |
3535 | +def get_ceph_nodes(): |
3536 | + hosts = [] |
3537 | + for r_id in relation_ids('ceph'): |
3538 | + for unit in related_units(r_id): |
3539 | + hosts.append(relation_get('private-address', unit=unit, rid=r_id)) |
3540 | + return hosts |
3541 | + |
3542 | + |
3543 | +def configure(service, key, auth): |
3544 | + create_keyring(service, key) |
3545 | + create_key_file(service, key) |
3546 | + hosts = get_ceph_nodes() |
3547 | + mon_hosts = ",".join(map(str, hosts)) |
3548 | + keyring = keyring_path(service) |
3549 | + with open('/etc/ceph/ceph.conf', 'w') as ceph_conf: |
3550 | + ceph_conf.write(CEPH_CONF % locals()) |
3551 | + modprobe_kernel_module('rbd') |
3552 | + |
3553 | + |
3554 | +def image_mapped(image_name): |
3555 | + (rc, out) = commands.getstatusoutput('rbd showmapped') |
3556 | + return image_name in out |
3557 | + |
3558 | + |
3559 | +def map_block_storage(service, pool, image): |
3560 | + cmd = [ |
3561 | + 'rbd', |
3562 | + 'map', |
3563 | + '%s/%s' % (pool, image), |
3564 | + '--user', |
3565 | + service, |
3566 | + '--secret', |
3567 | + keyfile_path(service), |
3568 | + ] |
3569 | + check_call(cmd) |
3570 | + |
3571 | + |
3572 | +def filesystem_mounted(fs): |
3573 | + return fs in [f for m, f in mounts()] |
3574 | + |
3575 | + |
3576 | +def make_filesystem(blk_device, fstype='ext4', timeout=10): |
3577 | + count = 0 |
3578 | + e_noent = os.errno.ENOENT |
3579 | + while not os.path.exists(blk_device): |
3580 | + if count >= timeout: |
3581 | + log('ceph: gave up waiting on block device %s' % blk_device, |
3582 | + level=ERROR) |
3583 | + raise IOError(e_noent, os.strerror(e_noent), blk_device) |
3584 | + log('ceph: waiting for block device %s to appear' % blk_device, |
3585 | + level=INFO) |
3586 | + count += 1 |
3587 | + time.sleep(1) |
3588 | + else: |
3589 | + log('ceph: Formatting block device %s as filesystem %s.' % |
3590 | + (blk_device, fstype), level=INFO) |
3591 | + check_call(['mkfs', '-t', fstype, blk_device]) |
3592 | + |
3593 | + |
3594 | +def place_data_on_ceph(service, blk_device, data_src_dst, fstype='ext4'): |
3595 | + # mount block device into /mnt |
3596 | + mount(blk_device, '/mnt') |
3597 | + |
3598 | + # copy data to /mnt |
3599 | + try: |
3600 | + copy_files(data_src_dst, '/mnt') |
3601 | + except: |
3602 | + pass |
3603 | + |
3604 | + # umount block device |
3605 | + umount('/mnt') |
3606 | + |
3607 | + _dir = os.stat(data_src_dst) |
3608 | + uid = _dir.st_uid |
3609 | + gid = _dir.st_gid |
3610 | + |
3611 | + # re-mount where the data should originally be |
3612 | + mount(blk_device, data_src_dst, persist=True) |
3613 | + |
3614 | + # ensure original ownership of new mount. |
3615 | + cmd = ['chown', '-R', '%s:%s' % (uid, gid), data_src_dst] |
3616 | + check_call(cmd) |
3617 | + |
3618 | + |
3619 | +# TODO: re-use |
3620 | +def modprobe_kernel_module(module): |
3621 | + log('ceph: Loading kernel module', level=INFO) |
3622 | + cmd = ['modprobe', module] |
3623 | + check_call(cmd) |
3624 | + cmd = 'echo %s >> /etc/modules' % module |
3625 | + check_call(cmd, shell=True) |
3626 | + |
3627 | + |
3628 | +def copy_files(src, dst, symlinks=False, ignore=None): |
3629 | + for item in os.listdir(src): |
3630 | + s = os.path.join(src, item) |
3631 | + d = os.path.join(dst, item) |
3632 | + if os.path.isdir(s): |
3633 | + shutil.copytree(s, d, symlinks, ignore) |
3634 | + else: |
3635 | + shutil.copy2(s, d) |
3636 | + |
3637 | + |
3638 | +def ensure_ceph_storage(service, pool, rbd_img, sizemb, mount_point, |
3639 | + blk_device, fstype, system_services=[]): |
3640 | + """ |
3641 | + To be called from the current cluster leader. |
3642 | + Ensures given pool and RBD image exists, is mapped to a block device, |
3643 | + and the device is formatted and mounted at the given mount_point. |
3644 | + |
3645 | + If formatting a device for the first time, data existing at mount_point |
3646 | + will be migrated to the RBD device before being remounted. |
3647 | + |
3648 | + All services listed in system_services will be stopped prior to data |
3649 | + migration and restarted when complete. |
3650 | + """ |
3651 | + # Ensure pool, RBD image, RBD mappings are in place. |
3652 | + if not pool_exists(service, pool): |
3653 | + log('ceph: Creating new pool %s.' % pool, level=INFO) |
3654 | + create_pool(service, pool) |
3655 | + |
3656 | + if not rbd_exists(service, pool, rbd_img): |
3657 | + log('ceph: Creating RBD image (%s).' % rbd_img, level=INFO) |
3658 | + create_rbd_image(service, pool, rbd_img, sizemb) |
3659 | + |
3660 | + if not image_mapped(rbd_img): |
3661 | + log('ceph: Mapping RBD Image as a Block Device.', level=INFO) |
3662 | + map_block_storage(service, pool, rbd_img) |
3663 | + |
3664 | + # make file system |
3665 | + # TODO: What happens if for whatever reason this is run again and |
3666 | + # the data is already in the rbd device and/or is mounted?? |
3667 | + # When it is mounted already, it will fail to make the fs |
3668 | + # XXX: This is really sketchy! Need to at least add an fstab entry |
3669 | + # otherwise this hook will blow away existing data if its executed |
3670 | + # after a reboot. |
3671 | + if not filesystem_mounted(mount_point): |
3672 | + make_filesystem(blk_device, fstype) |
3673 | + |
3674 | + for svc in system_services: |
3675 | + if running(svc): |
3676 | + log('Stopping services %s prior to migrating data.' % svc, |
3677 | + level=INFO) |
3678 | + service_stop(svc) |
3679 | + |
3680 | + place_data_on_ceph(service, blk_device, mount_point, fstype) |
3681 | + |
3682 | + for svc in system_services: |
3683 | + service_start(svc) |
3684 | |
3685 | === added file 'lib/charm-helpers/charmhelpers/contrib/hahelpers/cluster.py' |
3686 | --- lib/charm-helpers/charmhelpers/contrib/hahelpers/cluster.py 1970-01-01 00:00:00 +0000 |
3687 | +++ lib/charm-helpers/charmhelpers/contrib/hahelpers/cluster.py 2013-09-26 06:24:47 +0000 |
3688 | @@ -0,0 +1,181 @@ |
3689 | +# |
3690 | +# Copyright 2012 Canonical Ltd. |
3691 | +# |
3692 | +# Authors: |
3693 | +# James Page <james.page@ubuntu.com> |
3694 | +# Adam Gandelman <adamg@ubuntu.com> |
3695 | +# |
3696 | + |
3697 | +import subprocess |
3698 | +import os |
3699 | + |
3700 | +from socket import gethostname as get_unit_hostname |
3701 | + |
3702 | +from charmhelpers.core.hookenv import ( |
3703 | + log, |
3704 | + relation_ids, |
3705 | + related_units as relation_list, |
3706 | + relation_get, |
3707 | + config as config_get, |
3708 | + INFO, |
3709 | + ERROR, |
3710 | + unit_get, |
3711 | +) |
3712 | + |
3713 | + |
3714 | +class HAIncompleteConfig(Exception): |
3715 | + pass |
3716 | + |
3717 | + |
3718 | +def is_clustered(): |
3719 | + for r_id in (relation_ids('ha') or []): |
3720 | + for unit in (relation_list(r_id) or []): |
3721 | + clustered = relation_get('clustered', |
3722 | + rid=r_id, |
3723 | + unit=unit) |
3724 | + if clustered: |
3725 | + return True |
3726 | + return False |
3727 | + |
3728 | + |
3729 | +def is_leader(resource): |
3730 | + cmd = [ |
3731 | + "crm", "resource", |
3732 | + "show", resource |
3733 | + ] |
3734 | + try: |
3735 | + status = subprocess.check_output(cmd) |
3736 | + except subprocess.CalledProcessError: |
3737 | + return False |
3738 | + else: |
3739 | + if get_unit_hostname() in status: |
3740 | + return True |
3741 | + else: |
3742 | + return False |
3743 | + |
3744 | + |
3745 | +def peer_units(): |
3746 | + peers = [] |
3747 | + for r_id in (relation_ids('cluster') or []): |
3748 | + for unit in (relation_list(r_id) or []): |
3749 | + peers.append(unit) |
3750 | + return peers |
3751 | + |
3752 | + |
3753 | +def oldest_peer(peers): |
3754 | + local_unit_no = int(os.getenv('JUJU_UNIT_NAME').split('/')[1]) |
3755 | + for peer in peers: |
3756 | + remote_unit_no = int(peer.split('/')[1]) |
3757 | + if remote_unit_no < local_unit_no: |
3758 | + return False |
3759 | + return True |
3760 | + |
3761 | + |
3762 | +def eligible_leader(resource): |
3763 | + if is_clustered(): |
3764 | + if not is_leader(resource): |
3765 | + log('Deferring action to CRM leader.', level=INFO) |
3766 | + return False |
3767 | + else: |
3768 | + peers = peer_units() |
3769 | + if peers and not oldest_peer(peers): |
3770 | + log('Deferring action to oldest service unit.', level=INFO) |
3771 | + return False |
3772 | + return True |
3773 | + |
3774 | + |
3775 | +def https(): |
3776 | + ''' |
3777 | + Determines whether enough data has been provided in configuration |
3778 | + or relation data to configure HTTPS |
3779 | + . |
3780 | + returns: boolean |
3781 | + ''' |
3782 | + if config_get('use-https') == "yes": |
3783 | + return True |
3784 | + if config_get('ssl_cert') and config_get('ssl_key'): |
3785 | + return True |
3786 | + for r_id in relation_ids('identity-service'): |
3787 | + for unit in relation_list(r_id): |
3788 | + if None not in [ |
3789 | + relation_get('https_keystone', rid=r_id, unit=unit), |
3790 | + relation_get('ssl_cert', rid=r_id, unit=unit), |
3791 | + relation_get('ssl_key', rid=r_id, unit=unit), |
3792 | + relation_get('ca_cert', rid=r_id, unit=unit), |
3793 | + ]: |
3794 | + return True |
3795 | + return False |
3796 | + |
3797 | + |
3798 | +def determine_api_port(public_port): |
3799 | + ''' |
3800 | + Determine correct API server listening port based on |
3801 | + existence of HTTPS reverse proxy and/or haproxy. |
3802 | + |
3803 | + public_port: int: standard public port for given service |
3804 | + |
3805 | + returns: int: the correct listening port for the API service |
3806 | + ''' |
3807 | + i = 0 |
3808 | + if len(peer_units()) > 0 or is_clustered(): |
3809 | + i += 1 |
3810 | + if https(): |
3811 | + i += 1 |
3812 | + return public_port - (i * 10) |
3813 | + |
3814 | + |
3815 | +def determine_haproxy_port(public_port): |
3816 | + ''' |
3817 | + Description: Determine correct proxy listening port based on public IP + |
3818 | + existence of HTTPS reverse proxy. |
3819 | + |
3820 | + public_port: int: standard public port for given service |
3821 | + |
3822 | + returns: int: the correct listening port for the HAProxy service |
3823 | + ''' |
3824 | + i = 0 |
3825 | + if https(): |
3826 | + i += 1 |
3827 | + return public_port - (i * 10) |
3828 | + |
3829 | + |
3830 | +def get_hacluster_config(): |
3831 | + ''' |
3832 | + Obtains all relevant configuration from charm configuration required |
3833 | + for initiating a relation to hacluster: |
3834 | + |
3835 | + ha-bindiface, ha-mcastport, vip, vip_iface, vip_cidr |
3836 | + |
3837 | + returns: dict: A dict containing settings keyed by setting name. |
3838 | + raises: HAIncompleteConfig if settings are missing. |
3839 | + ''' |
3840 | + settings = ['ha-bindiface', 'ha-mcastport', 'vip', 'vip_iface', 'vip_cidr'] |
3841 | + conf = {} |
3842 | + for setting in settings: |
3843 | + conf[setting] = config_get(setting) |
3844 | + missing = [] |
3845 | + [missing.append(s) for s, v in conf.iteritems() if v is None] |
3846 | + if missing: |
3847 | + log('Insufficient config data to configure hacluster.', level=ERROR) |
3848 | + raise HAIncompleteConfig |
3849 | + return conf |
3850 | + |
3851 | + |
3852 | +def canonical_url(configs, vip_setting='vip'): |
3853 | + ''' |
3854 | + Returns the correct HTTP URL to this host given the state of HTTPS |
3855 | + configuration and hacluster. |
3856 | + |
3857 | + :configs : OSTemplateRenderer: A config tempating object to inspect for |
3858 | + a complete https context. |
3859 | + :vip_setting: str: Setting in charm config that specifies |
3860 | + VIP address. |
3861 | + ''' |
3862 | + scheme = 'http' |
3863 | + if 'https' in configs.complete_contexts(): |
3864 | + scheme = 'https' |
3865 | + if is_clustered(): |
3866 | + addr = config_get(vip_setting) |
3867 | + else: |
3868 | + addr = unit_get('private-address') |
3869 | + return '%s://%s' % (scheme, addr) |
3870 | |
3871 | === added directory 'lib/charm-helpers/charmhelpers/contrib/jujugui' |
3872 | === added file 'lib/charm-helpers/charmhelpers/contrib/jujugui/IMPORT' |
3873 | --- lib/charm-helpers/charmhelpers/contrib/jujugui/IMPORT 1970-01-01 00:00:00 +0000 |
3874 | +++ lib/charm-helpers/charmhelpers/contrib/jujugui/IMPORT 2013-09-26 06:24:47 +0000 |
3875 | @@ -0,0 +1,4 @@ |
3876 | +Source: lp:charms/juju-gui |
3877 | + |
3878 | +juju-gui/hooks/utils.py -> charm-helpers/charmhelpers/contrib/jujugui/utils.py |
3879 | +juju-gui/tests/test_utils.py -> charm-helpers/tests/contrib/jujugui/test_utils.py |
3880 | |
3881 | === added file 'lib/charm-helpers/charmhelpers/contrib/jujugui/__init__.py' |
3882 | === added file 'lib/charm-helpers/charmhelpers/contrib/jujugui/utils.py' |
3883 | --- lib/charm-helpers/charmhelpers/contrib/jujugui/utils.py 1970-01-01 00:00:00 +0000 |
3884 | +++ lib/charm-helpers/charmhelpers/contrib/jujugui/utils.py 2013-09-26 06:24:47 +0000 |
3885 | @@ -0,0 +1,602 @@ |
3886 | +"""Juju GUI charm utilities.""" |
3887 | + |
3888 | +__all__ = [ |
3889 | + 'AGENT', |
3890 | + 'APACHE', |
3891 | + 'API_PORT', |
3892 | + 'CURRENT_DIR', |
3893 | + 'HAPROXY', |
3894 | + 'IMPROV', |
3895 | + 'JUJU_DIR', |
3896 | + 'JUJU_GUI_DIR', |
3897 | + 'JUJU_GUI_SITE', |
3898 | + 'JUJU_PEM', |
3899 | + 'WEB_PORT', |
3900 | + 'bzr_checkout', |
3901 | + 'chain', |
3902 | + 'cmd_log', |
3903 | + 'fetch_api', |
3904 | + 'fetch_gui', |
3905 | + 'find_missing_packages', |
3906 | + 'first_path_in_dir', |
3907 | + 'get_api_address', |
3908 | + 'get_npm_cache_archive_url', |
3909 | + 'get_release_file_url', |
3910 | + 'get_staging_dependencies', |
3911 | + 'get_zookeeper_address', |
3912 | + 'legacy_juju', |
3913 | + 'log_hook', |
3914 | + 'merge', |
3915 | + 'parse_source', |
3916 | + 'prime_npm_cache', |
3917 | + 'render_to_file', |
3918 | + 'save_or_create_certificates', |
3919 | + 'setup_apache', |
3920 | + 'setup_gui', |
3921 | + 'start_agent', |
3922 | + 'start_gui', |
3923 | + 'start_improv', |
3924 | + 'write_apache_config', |
3925 | +] |
3926 | + |
3927 | +from contextlib import contextmanager |
3928 | +import errno |
3929 | +import json |
3930 | +import os |
3931 | +import logging |
3932 | +import shutil |
3933 | +from subprocess import CalledProcessError |
3934 | +import tempfile |
3935 | +from urlparse import urlparse |
3936 | + |
3937 | +import apt |
3938 | +import tempita |
3939 | + |
3940 | +from launchpadlib.launchpad import Launchpad |
3941 | +from shelltoolbox import ( |
3942 | + Serializer, |
3943 | + apt_get_install, |
3944 | + command, |
3945 | + environ, |
3946 | + install_extra_repositories, |
3947 | + run, |
3948 | + script_name, |
3949 | + search_file, |
3950 | + su, |
3951 | +) |
3952 | +from charmhelpers.core.host import ( |
3953 | + service_start, |
3954 | +) |
3955 | +from charmhelpers.core.hookenv import ( |
3956 | + log, |
3957 | + config, |
3958 | + unit_get, |
3959 | +) |
3960 | + |
3961 | + |
3962 | +AGENT = 'juju-api-agent' |
3963 | +APACHE = 'apache2' |
3964 | +IMPROV = 'juju-api-improv' |
3965 | +HAPROXY = 'haproxy' |
3966 | + |
3967 | +API_PORT = 8080 |
3968 | +WEB_PORT = 8000 |
3969 | + |
3970 | +CURRENT_DIR = os.getcwd() |
3971 | +JUJU_DIR = os.path.join(CURRENT_DIR, 'juju') |
3972 | +JUJU_GUI_DIR = os.path.join(CURRENT_DIR, 'juju-gui') |
3973 | +JUJU_GUI_SITE = '/etc/apache2/sites-available/juju-gui' |
3974 | +JUJU_GUI_PORTS = '/etc/apache2/ports.conf' |
3975 | +JUJU_PEM = 'juju.includes-private-key.pem' |
3976 | +BUILD_REPOSITORIES = ('ppa:chris-lea/node.js-legacy',) |
3977 | +DEB_BUILD_DEPENDENCIES = ( |
3978 | + 'bzr', 'imagemagick', 'make', 'nodejs', 'npm', |
3979 | +) |
3980 | +DEB_STAGE_DEPENDENCIES = ( |
3981 | + 'zookeeper', |
3982 | +) |
3983 | + |
3984 | + |
3985 | +# Store the configuration from on invocation to the next. |
3986 | +config_json = Serializer('/tmp/config.json') |
3987 | +# Bazaar checkout command. |
3988 | +bzr_checkout = command('bzr', 'co', '--lightweight') |
3989 | +# Whether or not the charm is deployed using juju-core. |
3990 | +# If juju-core has been used to deploy the charm, an agent.conf file must |
3991 | +# be present in the charm parent directory. |
3992 | +legacy_juju = lambda: not os.path.exists( |
3993 | + os.path.join(CURRENT_DIR, '..', 'agent.conf')) |
3994 | + |
3995 | + |
3996 | +def _get_build_dependencies(): |
3997 | + """Install deb dependencies for building.""" |
3998 | + log('Installing build dependencies.') |
3999 | + cmd_log(install_extra_repositories(*BUILD_REPOSITORIES)) |
4000 | + cmd_log(apt_get_install(*DEB_BUILD_DEPENDENCIES)) |
4001 | + |
4002 | + |
4003 | +def get_api_address(unit_dir): |
4004 | + """Return the Juju API address stored in the uniter agent.conf file.""" |
4005 | + import yaml # python-yaml is only installed if juju-core is used. |
4006 | + # XXX 2013-03-27 frankban bug=1161443: |
4007 | + # currently the uniter agent.conf file does not include the API |
4008 | + # address. For now retrieve it from the machine agent file. |
4009 | + base_dir = os.path.abspath(os.path.join(unit_dir, '..')) |
4010 | + for dirname in os.listdir(base_dir): |
4011 | + if dirname.startswith('machine-'): |
4012 | + agent_conf = os.path.join(base_dir, dirname, 'agent.conf') |
4013 | + break |
4014 | + else: |
4015 | + raise IOError('Juju agent configuration file not found.') |
4016 | + contents = yaml.load(open(agent_conf)) |
4017 | + return contents['apiinfo']['addrs'][0] |
4018 | + |
4019 | + |
4020 | +def get_staging_dependencies(): |
4021 | + """Install deb dependencies for the stage (improv) environment.""" |
4022 | + log('Installing stage dependencies.') |
4023 | + cmd_log(apt_get_install(*DEB_STAGE_DEPENDENCIES)) |
4024 | + |
4025 | + |
4026 | +def first_path_in_dir(directory): |
4027 | + """Return the full path of the first file/dir in *directory*.""" |
4028 | + return os.path.join(directory, os.listdir(directory)[0]) |
4029 | + |
4030 | + |
4031 | +def _get_by_attr(collection, attr, value): |
4032 | + """Return the first item in collection having attr == value. |
4033 | + |
4034 | + Return None if the item is not found. |
4035 | + """ |
4036 | + for item in collection: |
4037 | + if getattr(item, attr) == value: |
4038 | + return item |
4039 | + |
4040 | + |
4041 | +def get_release_file_url(project, series_name, release_version): |
4042 | + """Return the URL of the release file hosted in Launchpad. |
4043 | + |
4044 | + The returned URL points to a release file for the given project, series |
4045 | + name and release version. |
4046 | + The argument *project* is a project object as returned by launchpadlib. |
4047 | + The arguments *series_name* and *release_version* are strings. If |
4048 | + *release_version* is None, the URL of the latest release will be returned. |
4049 | + """ |
4050 | + series = _get_by_attr(project.series, 'name', series_name) |
4051 | + if series is None: |
4052 | + raise ValueError('%r: series not found' % series_name) |
4053 | + # Releases are returned by Launchpad in reverse date order. |
4054 | + releases = list(series.releases) |
4055 | + if not releases: |
4056 | + raise ValueError('%r: series does not contain releases' % series_name) |
4057 | + if release_version is not None: |
4058 | + release = _get_by_attr(releases, 'version', release_version) |
4059 | + if release is None: |
4060 | + raise ValueError('%r: release not found' % release_version) |
4061 | + releases = [release] |
4062 | + for release in releases: |
4063 | + for file_ in release.files: |
4064 | + if str(file_).endswith('.tgz'): |
4065 | + return file_.file_link |
4066 | + raise ValueError('%r: file not found' % release_version) |
4067 | + |
4068 | + |
4069 | +def get_zookeeper_address(agent_file_path): |
4070 | + """Retrieve the Zookeeper address contained in the given *agent_file_path*. |
4071 | + |
4072 | + The *agent_file_path* is a path to a file containing a line similar to the |
4073 | + following:: |
4074 | + |
4075 | + env JUJU_ZOOKEEPER="address" |
4076 | + """ |
4077 | + line = search_file('JUJU_ZOOKEEPER', agent_file_path).strip() |
4078 | + return line.split('=')[1].strip('"') |
4079 | + |
4080 | + |
4081 | +@contextmanager |
4082 | +def log_hook(): |
4083 | + """Log when a hook starts and stops its execution. |
4084 | + |
4085 | + Also log to stdout possible CalledProcessError exceptions raised executing |
4086 | + the hook. |
4087 | + """ |
4088 | + script = script_name() |
4089 | + log(">>> Entering {}".format(script)) |
4090 | + try: |
4091 | + yield |
4092 | + except CalledProcessError as err: |
4093 | + log('Exception caught:') |
4094 | + log(err.output) |
4095 | + raise |
4096 | + finally: |
4097 | + log("<<< Exiting {}".format(script)) |
4098 | + |
4099 | + |
4100 | +def parse_source(source): |
4101 | + """Parse the ``juju-gui-source`` option. |
4102 | + |
4103 | + Return a tuple of two elements representing info on how to deploy Juju GUI. |
4104 | + Examples: |
4105 | + - ('stable', None): latest stable release; |
4106 | + - ('stable', '0.1.0'): stable release v0.1.0; |
4107 | + - ('trunk', None): latest trunk release; |
4108 | + - ('trunk', '0.1.0+build.1'): trunk release v0.1.0 bzr revision 1; |
4109 | + - ('branch', 'lp:juju-gui'): release is made from a branch; |
4110 | + - ('url', 'http://example.com/gui'): release from a downloaded file. |
4111 | + """ |
4112 | + if source.startswith('url:'): |
4113 | + source = source[4:] |
4114 | + # Support file paths, including relative paths. |
4115 | + if urlparse(source).scheme == '': |
4116 | + if not source.startswith('/'): |
4117 | + source = os.path.join(os.path.abspath(CURRENT_DIR), source) |
4118 | + source = "file://%s" % source |
4119 | + return 'url', source |
4120 | + if source in ('stable', 'trunk'): |
4121 | + return source, None |
4122 | + if source.startswith('lp:') or source.startswith('http://'): |
4123 | + return 'branch', source |
4124 | + if 'build' in source: |
4125 | + return 'trunk', source |
4126 | + return 'stable', source |
4127 | + |
4128 | + |
4129 | +def render_to_file(template_name, context, destination): |
4130 | + """Render the given *template_name* into *destination* using *context*. |
4131 | + |
4132 | + The tempita template language is used to render contents |
4133 | + (see http://pythonpaste.org/tempita/). |
4134 | + The argument *template_name* is the name or path of the template file: |
4135 | + it may be either a path relative to ``../config`` or an absolute path. |
4136 | + The argument *destination* is a file path. |
4137 | + The argument *context* is a dict-like object. |
4138 | + """ |
4139 | + template_path = os.path.abspath(template_name) |
4140 | + template = tempita.Template.from_filename(template_path) |
4141 | + with open(destination, 'w') as stream: |
4142 | + stream.write(template.substitute(context)) |
4143 | + |
4144 | + |
4145 | +results_log = None |
4146 | + |
4147 | + |
4148 | +def _setupLogging(): |
4149 | + global results_log |
4150 | + if results_log is not None: |
4151 | + return |
4152 | + cfg = config() |
4153 | + logging.basicConfig( |
4154 | + filename=cfg['command-log-file'], |
4155 | + level=logging.INFO, |
4156 | + format="%(asctime)s: %(name)s@%(levelname)s %(message)s") |
4157 | + results_log = logging.getLogger('juju-gui') |
4158 | + |
4159 | + |
4160 | +def cmd_log(results): |
4161 | + global results_log |
4162 | + if not results: |
4163 | + return |
4164 | + if results_log is None: |
4165 | + _setupLogging() |
4166 | + # Since 'results' may be multi-line output, start it on a separate line |
4167 | + # from the logger timestamp, etc. |
4168 | + results_log.info('\n' + results) |
4169 | + |
4170 | + |
4171 | +def start_improv(staging_env, ssl_cert_path, |
4172 | + config_path='/etc/init/juju-api-improv.conf'): |
4173 | + """Start a simulated juju environment using ``improv.py``.""" |
4174 | + log('Setting up staging start up script.') |
4175 | + context = { |
4176 | + 'juju_dir': JUJU_DIR, |
4177 | + 'keys': ssl_cert_path, |
4178 | + 'port': API_PORT, |
4179 | + 'staging_env': staging_env, |
4180 | + } |
4181 | + render_to_file('config/juju-api-improv.conf.template', context, config_path) |
4182 | + log('Starting the staging backend.') |
4183 | + with su('root'): |
4184 | + service_start(IMPROV) |
4185 | + |
4186 | + |
4187 | +def start_agent( |
4188 | + ssl_cert_path, config_path='/etc/init/juju-api-agent.conf', |
4189 | + read_only=False): |
4190 | + """Start the Juju agent and connect to the current environment.""" |
4191 | + # Retrieve the Zookeeper address from the start up script. |
4192 | + unit_dir = os.path.realpath(os.path.join(CURRENT_DIR, '..')) |
4193 | + agent_file = '/etc/init/juju-{0}.conf'.format(os.path.basename(unit_dir)) |
4194 | + zookeeper = get_zookeeper_address(agent_file) |
4195 | + log('Setting up API agent start up script.') |
4196 | + context = { |
4197 | + 'juju_dir': JUJU_DIR, |
4198 | + 'keys': ssl_cert_path, |
4199 | + 'port': API_PORT, |
4200 | + 'zookeeper': zookeeper, |
4201 | + 'read_only': read_only |
4202 | + } |
4203 | + render_to_file('config/juju-api-agent.conf.template', context, config_path) |
4204 | + log('Starting API agent.') |
4205 | + with su('root'): |
4206 | + service_start(AGENT) |
4207 | + |
4208 | + |
4209 | +def start_gui( |
4210 | + console_enabled, login_help, readonly, in_staging, ssl_cert_path, |
4211 | + charmworld_url, serve_tests, haproxy_path='/etc/haproxy/haproxy.cfg', |
4212 | + config_js_path=None, secure=True, sandbox=False): |
4213 | + """Set up and start the Juju GUI server.""" |
4214 | + with su('root'): |
4215 | + run('chown', '-R', 'ubuntu:', JUJU_GUI_DIR) |
4216 | + # XXX 2013-02-05 frankban bug=1116320: |
4217 | + # External insecure resources are still loaded when testing in the |
4218 | + # debug environment. For now, switch to the production environment if |
4219 | + # the charm is configured to serve tests. |
4220 | + if in_staging and not serve_tests: |
4221 | + build_dirname = 'build-debug' |
4222 | + else: |
4223 | + build_dirname = 'build-prod' |
4224 | + build_dir = os.path.join(JUJU_GUI_DIR, build_dirname) |
4225 | + log('Generating the Juju GUI configuration file.') |
4226 | + is_legacy_juju = legacy_juju() |
4227 | + user, password = None, None |
4228 | + if (is_legacy_juju and in_staging) or sandbox: |
4229 | + user, password = 'admin', 'admin' |
4230 | + else: |
4231 | + user, password = None, None |
4232 | + |
4233 | + api_backend = 'python' if is_legacy_juju else 'go' |
4234 | + if secure: |
4235 | + protocol = 'wss' |
4236 | + else: |
4237 | + log('Running in insecure mode! Port 80 will serve unencrypted.') |
4238 | + protocol = 'ws' |
4239 | + |
4240 | + context = { |
4241 | + 'raw_protocol': protocol, |
4242 | + 'address': unit_get('public-address'), |
4243 | + 'console_enabled': json.dumps(console_enabled), |
4244 | + 'login_help': json.dumps(login_help), |
4245 | + 'password': json.dumps(password), |
4246 | + 'api_backend': json.dumps(api_backend), |
4247 | + 'readonly': json.dumps(readonly), |
4248 | + 'user': json.dumps(user), |
4249 | + 'protocol': json.dumps(protocol), |
4250 | + 'sandbox': json.dumps(sandbox), |
4251 | + 'charmworld_url': json.dumps(charmworld_url), |
4252 | + } |
4253 | + if config_js_path is None: |
4254 | + config_js_path = os.path.join( |
4255 | + build_dir, 'juju-ui', 'assets', 'config.js') |
4256 | + render_to_file('config/config.js.template', context, config_js_path) |
4257 | + |
4258 | + write_apache_config(build_dir, serve_tests) |
4259 | + |
4260 | + log('Generating haproxy configuration file.') |
4261 | + if is_legacy_juju: |
4262 | + # The PyJuju API agent is listening on localhost. |
4263 | + api_address = '127.0.0.1:{0}'.format(API_PORT) |
4264 | + else: |
4265 | + # Retrieve the juju-core API server address. |
4266 | + api_address = get_api_address(os.path.join(CURRENT_DIR, '..')) |
4267 | + context = { |
4268 | + 'api_address': api_address, |
4269 | + 'api_pem': JUJU_PEM, |
4270 | + 'legacy_juju': is_legacy_juju, |
4271 | + 'ssl_cert_path': ssl_cert_path, |
4272 | + # In PyJuju environments, use the same certificate for both HTTPS and |
4273 | + # WebSocket connections. In juju-core the system already has the proper |
4274 | + # certificate installed. |
4275 | + 'web_pem': JUJU_PEM, |
4276 | + 'web_port': WEB_PORT, |
4277 | + 'secure': secure |
4278 | + } |
4279 | + render_to_file('config/haproxy.cfg.template', context, haproxy_path) |
4280 | + log('Starting Juju GUI.') |
4281 | + |
4282 | + |
4283 | +def write_apache_config(build_dir, serve_tests=False): |
4284 | + log('Generating the apache site configuration file.') |
4285 | + context = { |
4286 | + 'port': WEB_PORT, |
4287 | + 'serve_tests': serve_tests, |
4288 | + 'server_root': build_dir, |
4289 | + 'tests_root': os.path.join(JUJU_GUI_DIR, 'test', ''), |
4290 | + } |
4291 | + render_to_file('config/apache-ports.template', context, JUJU_GUI_PORTS) |
4292 | + render_to_file('config/apache-site.template', context, JUJU_GUI_SITE) |
4293 | + |
4294 | + |
4295 | +def get_npm_cache_archive_url(Launchpad=Launchpad): |
4296 | + """Figure out the URL of the most recent NPM cache archive on Launchpad.""" |
4297 | + launchpad = Launchpad.login_anonymously('Juju GUI charm', 'production') |
4298 | + project = launchpad.projects['juju-gui'] |
4299 | + # Find the URL of the most recently created NPM cache archive. |
4300 | + npm_cache_url = get_release_file_url(project, 'npm-cache', None) |
4301 | + return npm_cache_url |
4302 | + |
4303 | + |
4304 | +def prime_npm_cache(npm_cache_url): |
4305 | + """Download NPM cache archive and prime the NPM cache with it.""" |
4306 | + # Download the cache archive and then uncompress it into the NPM cache. |
4307 | + npm_cache_archive = os.path.join(CURRENT_DIR, 'npm-cache.tgz') |
4308 | + cmd_log(run('curl', '-L', '-o', npm_cache_archive, npm_cache_url)) |
4309 | + npm_cache_dir = os.path.expanduser('~/.npm') |
4310 | + # The NPM cache directory probably does not exist, so make it if not. |
4311 | + try: |
4312 | + os.mkdir(npm_cache_dir) |
4313 | + except OSError, e: |
4314 | + # If the directory already exists then ignore the error. |
4315 | + if e.errno != errno.EEXIST: # File exists. |
4316 | + raise |
4317 | + uncompress = command('tar', '-x', '-z', '-C', npm_cache_dir, '-f') |
4318 | + cmd_log(uncompress(npm_cache_archive)) |
4319 | + |
4320 | + |
4321 | +def fetch_gui(juju_gui_source, logpath): |
4322 | + """Retrieve the Juju GUI release/branch.""" |
4323 | + # Retrieve a Juju GUI release. |
4324 | + origin, version_or_branch = parse_source(juju_gui_source) |
4325 | + if origin == 'branch': |
4326 | + # Make sure we have the dependencies necessary for us to actually make |
4327 | + # a build. |
4328 | + _get_build_dependencies() |
4329 | + # Create a release starting from a branch. |
4330 | + juju_gui_source_dir = os.path.join(CURRENT_DIR, 'juju-gui-source') |
4331 | + log('Retrieving Juju GUI source checkout from %s.' % version_or_branch) |
4332 | + cmd_log(run('rm', '-rf', juju_gui_source_dir)) |
4333 | + cmd_log(bzr_checkout(version_or_branch, juju_gui_source_dir)) |
4334 | + log('Preparing a Juju GUI release.') |
4335 | + logdir = os.path.dirname(logpath) |
4336 | + fd, name = tempfile.mkstemp(prefix='make-distfile-', dir=logdir) |
4337 | + log('Output from "make distfile" sent to %s' % name) |
4338 | + with environ(NO_BZR='1'): |
4339 | + run('make', '-C', juju_gui_source_dir, 'distfile', |
4340 | + stdout=fd, stderr=fd) |
4341 | + release_tarball = first_path_in_dir( |
4342 | + os.path.join(juju_gui_source_dir, 'releases')) |
4343 | + else: |
4344 | + log('Retrieving Juju GUI release.') |
4345 | + if origin == 'url': |
4346 | + file_url = version_or_branch |
4347 | + else: |
4348 | + # Retrieve a release from Launchpad. |
4349 | + launchpad = Launchpad.login_anonymously( |
4350 | + 'Juju GUI charm', 'production') |
4351 | + project = launchpad.projects['juju-gui'] |
4352 | + file_url = get_release_file_url(project, origin, version_or_branch) |
4353 | + log('Downloading release file from %s.' % file_url) |
4354 | + release_tarball = os.path.join(CURRENT_DIR, 'release.tgz') |
4355 | + cmd_log(run('curl', '-L', '-o', release_tarball, file_url)) |
4356 | + return release_tarball |
4357 | + |
4358 | + |
4359 | +def fetch_api(juju_api_branch): |
4360 | + """Retrieve the Juju branch.""" |
4361 | + # Retrieve Juju API source checkout. |
4362 | + log('Retrieving Juju API source checkout.') |
4363 | + cmd_log(run('rm', '-rf', JUJU_DIR)) |
4364 | + cmd_log(bzr_checkout(juju_api_branch, JUJU_DIR)) |
4365 | + |
4366 | + |
4367 | +def setup_gui(release_tarball): |
4368 | + """Set up Juju GUI.""" |
4369 | + # Uncompress the release tarball. |
4370 | + log('Installing Juju GUI.') |
4371 | + release_dir = os.path.join(CURRENT_DIR, 'release') |
4372 | + cmd_log(run('rm', '-rf', release_dir)) |
4373 | + os.mkdir(release_dir) |
4374 | + uncompress = command('tar', '-x', '-z', '-C', release_dir, '-f') |
4375 | + cmd_log(uncompress(release_tarball)) |
4376 | + # Link the Juju GUI dir to the contents of the release tarball. |
4377 | + cmd_log(run('ln', '-sf', first_path_in_dir(release_dir), JUJU_GUI_DIR)) |
4378 | + |
4379 | + |
4380 | +def setup_apache(): |
4381 | + """Set up apache.""" |
4382 | + log('Setting up apache.') |
4383 | + if not os.path.exists(JUJU_GUI_SITE): |
4384 | + cmd_log(run('touch', JUJU_GUI_SITE)) |
4385 | + cmd_log(run('chown', 'ubuntu:', JUJU_GUI_SITE)) |
4386 | + cmd_log( |
4387 | + run('ln', '-s', JUJU_GUI_SITE, |
4388 | + '/etc/apache2/sites-enabled/juju-gui')) |
4389 | + |
4390 | + if not os.path.exists(JUJU_GUI_PORTS): |
4391 | + cmd_log(run('touch', JUJU_GUI_PORTS)) |
4392 | + cmd_log(run('chown', 'ubuntu:', JUJU_GUI_PORTS)) |
4393 | + |
4394 | + with su('root'): |
4395 | + run('a2dissite', 'default') |
4396 | + run('a2ensite', 'juju-gui') |
4397 | + |
4398 | + |
4399 | +def save_or_create_certificates( |
4400 | + ssl_cert_path, ssl_cert_contents, ssl_key_contents): |
4401 | + """Generate the SSL certificates. |
4402 | + |
4403 | + If both *ssl_cert_contents* and *ssl_key_contents* are provided, use them |
4404 | + as certificates; otherwise, generate them. |
4405 | + |
4406 | + Also create a pem file, suitable for use in the haproxy configuration, |
4407 | + concatenating the key and the certificate files. |
4408 | + """ |
4409 | + crt_path = os.path.join(ssl_cert_path, 'juju.crt') |
4410 | + key_path = os.path.join(ssl_cert_path, 'juju.key') |
4411 | + if not os.path.exists(ssl_cert_path): |
4412 | + os.makedirs(ssl_cert_path) |
4413 | + if ssl_cert_contents and ssl_key_contents: |
4414 | + # Save the provided certificates. |
4415 | + with open(crt_path, 'w') as cert_file: |
4416 | + cert_file.write(ssl_cert_contents) |
4417 | + with open(key_path, 'w') as key_file: |
4418 | + key_file.write(ssl_key_contents) |
4419 | + else: |
4420 | + # Generate certificates. |
4421 | + # See http://superuser.com/questions/226192/openssl-without-prompt |
4422 | + cmd_log(run( |
4423 | + 'openssl', 'req', '-new', '-newkey', 'rsa:4096', |
4424 | + '-days', '365', '-nodes', '-x509', '-subj', |
4425 | + # These are arbitrary test values for the certificate. |
4426 | + '/C=GB/ST=Juju/L=GUI/O=Ubuntu/CN=juju.ubuntu.com', |
4427 | + '-keyout', key_path, '-out', crt_path)) |
4428 | + # Generate the pem file. |
4429 | + pem_path = os.path.join(ssl_cert_path, JUJU_PEM) |
4430 | + if os.path.exists(pem_path): |
4431 | + os.remove(pem_path) |
4432 | + with open(pem_path, 'w') as pem_file: |
4433 | + shutil.copyfileobj(open(key_path), pem_file) |
4434 | + shutil.copyfileobj(open(crt_path), pem_file) |
4435 | + |
4436 | + |
4437 | +def find_missing_packages(*packages): |
4438 | + """Given a list of packages, return the packages which are not installed. |
4439 | + """ |
4440 | + cache = apt.Cache() |
4441 | + missing = set() |
4442 | + for pkg_name in packages: |
4443 | + try: |
4444 | + pkg = cache[pkg_name] |
4445 | + except KeyError: |
4446 | + missing.add(pkg_name) |
4447 | + continue |
4448 | + if pkg.is_installed: |
4449 | + continue |
4450 | + missing.add(pkg_name) |
4451 | + return missing |
4452 | + |
4453 | + |
4454 | +## Backend support decorators |
4455 | + |
4456 | +def chain(name): |
4457 | + """Helper method to compose a set of mixin objects into a callable. |
4458 | + |
4459 | + Each method is called in the context of its mixin instance, and its |
4460 | + argument is the Backend instance. |
4461 | + """ |
4462 | + # Chain method calls through all implementing mixins. |
4463 | + def method(self): |
4464 | + for mixin in self.mixins: |
4465 | + a_callable = getattr(type(mixin), name, None) |
4466 | + if a_callable: |
4467 | + a_callable(mixin, self) |
4468 | + |
4469 | + method.__name__ = name |
4470 | + return method |
4471 | + |
4472 | + |
4473 | +def merge(name): |
4474 | + """Helper to merge a property from a set of strategy objects |
4475 | + into a unified set. |
4476 | + """ |
4477 | + # Return merged property from every providing mixin as a set. |
4478 | + @property |
4479 | + def method(self): |
4480 | + result = set() |
4481 | + for mixin in self.mixins: |
4482 | + segment = getattr(type(mixin), name, None) |
4483 | + if segment and isinstance(segment, (list, tuple, set)): |
4484 | + result |= set(segment) |
4485 | + |
4486 | + return result |
4487 | + return method |
4488 | |
4489 | === added directory 'lib/charm-helpers/charmhelpers/contrib/network' |
4490 | === added file 'lib/charm-helpers/charmhelpers/contrib/network/__init__.py' |
4491 | === added directory 'lib/charm-helpers/charmhelpers/contrib/network/ovs' |
4492 | === added file 'lib/charm-helpers/charmhelpers/contrib/network/ovs/__init__.py' |
4493 | --- lib/charm-helpers/charmhelpers/contrib/network/ovs/__init__.py 1970-01-01 00:00:00 +0000 |
4494 | +++ lib/charm-helpers/charmhelpers/contrib/network/ovs/__init__.py 2013-09-26 06:24:47 +0000 |
4495 | @@ -0,0 +1,72 @@ |
4496 | +''' Helpers for interacting with OpenvSwitch ''' |
4497 | +import subprocess |
4498 | +import os |
4499 | +from charmhelpers.core.hookenv import ( |
4500 | + log, WARNING |
4501 | +) |
4502 | +from charmhelpers.core.host import ( |
4503 | + service |
4504 | +) |
4505 | + |
4506 | + |
4507 | +def add_bridge(name): |
4508 | + ''' Add the named bridge to openvswitch ''' |
4509 | + log('Creating bridge {}'.format(name)) |
4510 | + subprocess.check_call(["ovs-vsctl", "--", "--may-exist", "add-br", name]) |
4511 | + |
4512 | + |
4513 | +def del_bridge(name): |
4514 | + ''' Delete the named bridge from openvswitch ''' |
4515 | + log('Deleting bridge {}'.format(name)) |
4516 | + subprocess.check_call(["ovs-vsctl", "--", "--if-exists", "del-br", name]) |
4517 | + |
4518 | + |
4519 | +def add_bridge_port(name, port): |
4520 | + ''' Add a port to the named openvswitch bridge ''' |
4521 | + log('Adding port {} to bridge {}'.format(port, name)) |
4522 | + subprocess.check_call(["ovs-vsctl", "--", "--may-exist", "add-port", |
4523 | + name, port]) |
4524 | + subprocess.check_call(["ip", "link", "set", port, "up"]) |
4525 | + |
4526 | + |
4527 | +def del_bridge_port(name, port): |
4528 | + ''' Delete a port from the named openvswitch bridge ''' |
4529 | + log('Deleting port {} from bridge {}'.format(port, name)) |
4530 | + subprocess.check_call(["ovs-vsctl", "--", "--if-exists", "del-port", |
4531 | + name, port]) |
4532 | + subprocess.check_call(["ip", "link", "set", port, "down"]) |
4533 | + |
4534 | + |
4535 | +def set_manager(manager): |
4536 | + ''' Set the controller for the local openvswitch ''' |
4537 | + log('Setting manager for local ovs to {}'.format(manager)) |
4538 | + subprocess.check_call(['ovs-vsctl', 'set-manager', |
4539 | + 'ssl:{}'.format(manager)]) |
4540 | + |
4541 | + |
4542 | +CERT_PATH = '/etc/openvswitch/ovsclient-cert.pem' |
4543 | + |
4544 | + |
4545 | +def get_certificate(): |
4546 | + ''' Read openvswitch certificate from disk ''' |
4547 | + if os.path.exists(CERT_PATH): |
4548 | + log('Reading ovs certificate from {}'.format(CERT_PATH)) |
4549 | + with open(CERT_PATH, 'r') as cert: |
4550 | + full_cert = cert.read() |
4551 | + begin_marker = "-----BEGIN CERTIFICATE-----" |
4552 | + end_marker = "-----END CERTIFICATE-----" |
4553 | + begin_index = full_cert.find(begin_marker) |
4554 | + end_index = full_cert.rfind(end_marker) |
4555 | + if end_index == -1 or begin_index == -1: |
4556 | + raise RuntimeError("Certificate does not contain valid begin" |
4557 | + " and end markers.") |
4558 | + full_cert = full_cert[begin_index:(end_index + len(end_marker))] |
4559 | + return full_cert |
4560 | + else: |
4561 | + log('Certificate not found', level=WARNING) |
4562 | + return None |
4563 | + |
4564 | + |
4565 | +def full_restart(): |
4566 | + ''' Full restart and reload of openvswitch ''' |
4567 | + service('force-reload-kmod', 'openvswitch-switch') |
4568 | |
4569 | === added directory 'lib/charm-helpers/charmhelpers/contrib/openstack' |
4570 | === added file 'lib/charm-helpers/charmhelpers/contrib/openstack/__init__.py' |
4571 | === added file 'lib/charm-helpers/charmhelpers/contrib/openstack/context.py' |
4572 | --- lib/charm-helpers/charmhelpers/contrib/openstack/context.py 1970-01-01 00:00:00 +0000 |
4573 | +++ lib/charm-helpers/charmhelpers/contrib/openstack/context.py 2013-09-26 06:24:47 +0000 |
4574 | @@ -0,0 +1,294 @@ |
4575 | +import os |
4576 | + |
4577 | +from base64 import b64decode |
4578 | + |
4579 | +from subprocess import ( |
4580 | + check_call |
4581 | +) |
4582 | + |
4583 | +from charmhelpers.core.hookenv import ( |
4584 | + config, |
4585 | + local_unit, |
4586 | + log, |
4587 | + relation_get, |
4588 | + relation_ids, |
4589 | + related_units, |
4590 | + unit_get, |
4591 | +) |
4592 | + |
4593 | +from charmhelpers.contrib.hahelpers.cluster import ( |
4594 | + determine_api_port, |
4595 | + determine_haproxy_port, |
4596 | + https, |
4597 | + is_clustered, |
4598 | + peer_units, |
4599 | +) |
4600 | + |
4601 | +from charmhelpers.contrib.hahelpers.apache import ( |
4602 | + get_cert, |
4603 | + get_ca_cert, |
4604 | +) |
4605 | + |
4606 | +CA_CERT_PATH = '/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt' |
4607 | + |
4608 | + |
4609 | +class OSContextError(Exception): |
4610 | + pass |
4611 | + |
4612 | + |
4613 | +def context_complete(ctxt): |
4614 | + _missing = [] |
4615 | + for k, v in ctxt.iteritems(): |
4616 | + if v is None or v == '': |
4617 | + _missing.append(k) |
4618 | + if _missing: |
4619 | + log('Missing required data: %s' % ' '.join(_missing), level='INFO') |
4620 | + return False |
4621 | + return True |
4622 | + |
4623 | + |
4624 | +class OSContextGenerator(object): |
4625 | + interfaces = [] |
4626 | + |
4627 | + def __call__(self): |
4628 | + raise NotImplementedError |
4629 | + |
4630 | + |
4631 | +class SharedDBContext(OSContextGenerator): |
4632 | + interfaces = ['shared-db'] |
4633 | + |
4634 | + def __call__(self): |
4635 | + log('Generating template context for shared-db') |
4636 | + conf = config() |
4637 | + try: |
4638 | + database = conf['database'] |
4639 | + username = conf['database-user'] |
4640 | + except KeyError as e: |
4641 | + log('Could not generate shared_db context. ' |
4642 | + 'Missing required charm config options: %s.' % e) |
4643 | + raise OSContextError |
4644 | + ctxt = {} |
4645 | + for rid in relation_ids('shared-db'): |
4646 | + for unit in related_units(rid): |
4647 | + ctxt = { |
4648 | + 'database_host': relation_get('db_host', rid=rid, |
4649 | + unit=unit), |
4650 | + 'database': database, |
4651 | + 'database_user': username, |
4652 | + 'database_password': relation_get('password', rid=rid, |
4653 | + unit=unit) |
4654 | + } |
4655 | + if not context_complete(ctxt): |
4656 | + return {} |
4657 | + return ctxt |
4658 | + |
4659 | + |
4660 | +class IdentityServiceContext(OSContextGenerator): |
4661 | + interfaces = ['identity-service'] |
4662 | + |
4663 | + def __call__(self): |
4664 | + log('Generating template context for identity-service') |
4665 | + ctxt = {} |
4666 | + |
4667 | + for rid in relation_ids('identity-service'): |
4668 | + for unit in related_units(rid): |
4669 | + ctxt = { |
4670 | + 'service_port': relation_get('service_port', rid=rid, |
4671 | + unit=unit), |
4672 | + 'service_host': relation_get('service_host', rid=rid, |
4673 | + unit=unit), |
4674 | + 'auth_host': relation_get('auth_host', rid=rid, unit=unit), |
4675 | + 'auth_port': relation_get('auth_port', rid=rid, unit=unit), |
4676 | + 'admin_tenant_name': relation_get('service_tenant', |
4677 | + rid=rid, unit=unit), |
4678 | + 'admin_user': relation_get('service_username', rid=rid, |
4679 | + unit=unit), |
4680 | + 'admin_password': relation_get('service_password', rid=rid, |
4681 | + unit=unit), |
4682 | + # XXX: Hard-coded http. |
4683 | + 'service_protocol': 'http', |
4684 | + 'auth_protocol': 'http', |
4685 | + } |
4686 | + if not context_complete(ctxt): |
4687 | + return {} |
4688 | + return ctxt |
4689 | + |
4690 | + |
4691 | +class AMQPContext(OSContextGenerator): |
4692 | + interfaces = ['amqp'] |
4693 | + |
4694 | + def __call__(self): |
4695 | + log('Generating template context for amqp') |
4696 | + conf = config() |
4697 | + try: |
4698 | + username = conf['rabbit-user'] |
4699 | + vhost = conf['rabbit-vhost'] |
4700 | + except KeyError as e: |
4701 | + log('Could not generate shared_db context. ' |
4702 | + 'Missing required charm config options: %s.' % e) |
4703 | + raise OSContextError |
4704 | + |
4705 | + ctxt = {} |
4706 | + for rid in relation_ids('amqp'): |
4707 | + for unit in related_units(rid): |
4708 | + if relation_get('clustered', rid=rid, unit=unit): |
4709 | + rabbitmq_host = relation_get('vip', rid=rid, unit=unit) |
4710 | + else: |
4711 | + rabbitmq_host = relation_get('private-address', |
4712 | + rid=rid, unit=unit) |
4713 | + ctxt = { |
4714 | + 'rabbitmq_host': rabbitmq_host, |
4715 | + 'rabbitmq_user': username, |
4716 | + 'rabbitmq_password': relation_get('password', rid=rid, |
4717 | + unit=unit), |
4718 | + 'rabbitmq_virtual_host': vhost, |
4719 | + } |
4720 | + if not context_complete(ctxt): |
4721 | + return {} |
4722 | + return ctxt |
4723 | + |
4724 | + |
4725 | +class CephContext(OSContextGenerator): |
4726 | + interfaces = ['ceph'] |
4727 | + |
4728 | + def __call__(self): |
4729 | + '''This generates context for /etc/ceph/ceph.conf templates''' |
4730 | + log('Generating tmeplate context for ceph') |
4731 | + mon_hosts = [] |
4732 | + auth = None |
4733 | + for rid in relation_ids('ceph'): |
4734 | + for unit in related_units(rid): |
4735 | + mon_hosts.append(relation_get('private-address', rid=rid, |
4736 | + unit=unit)) |
4737 | + auth = relation_get('auth', rid=rid, unit=unit) |
4738 | + |
4739 | + ctxt = { |
4740 | + 'mon_hosts': ' '.join(mon_hosts), |
4741 | + 'auth': auth, |
4742 | + } |
4743 | + if not context_complete(ctxt): |
4744 | + return {} |
4745 | + return ctxt |
4746 | + |
4747 | + |
4748 | +class HAProxyContext(OSContextGenerator): |
4749 | + interfaces = ['cluster'] |
4750 | + |
4751 | + def __call__(self): |
4752 | + ''' |
4753 | + Builds half a context for the haproxy template, which describes |
4754 | + all peers to be included in the cluster. Each charm needs to include |
4755 | + its own context generator that describes the port mapping. |
4756 | + ''' |
4757 | + if not relation_ids('cluster'): |
4758 | + return {} |
4759 | + |
4760 | + cluster_hosts = {} |
4761 | + l_unit = local_unit().replace('/', '-') |
4762 | + cluster_hosts[l_unit] = unit_get('private-address') |
4763 | + |
4764 | + for rid in relation_ids('cluster'): |
4765 | + for unit in related_units(rid): |
4766 | + _unit = unit.replace('/', '-') |
4767 | + addr = relation_get('private-address', rid=rid, unit=unit) |
4768 | + cluster_hosts[_unit] = addr |
4769 | + |
4770 | + ctxt = { |
4771 | + 'units': cluster_hosts, |
4772 | + } |
4773 | + if len(cluster_hosts.keys()) > 1: |
4774 | + # Enable haproxy when we have enough peers. |
4775 | + log('Ensuring haproxy enabled in /etc/default/haproxy.') |
4776 | + with open('/etc/default/haproxy', 'w') as out: |
4777 | + out.write('ENABLED=1\n') |
4778 | + return ctxt |
4779 | + log('HAProxy context is incomplete, this unit has no peers.') |
4780 | + return {} |
4781 | + |
4782 | + |
4783 | +class ImageServiceContext(OSContextGenerator): |
4784 | + interfaces = ['image-servce'] |
4785 | + |
4786 | + def __call__(self): |
4787 | + ''' |
4788 | + Obtains the glance API server from the image-service relation. Useful |
4789 | + in nova and cinder (currently). |
4790 | + ''' |
4791 | + log('Generating template context for image-service.') |
4792 | + rids = relation_ids('image-service') |
4793 | + if not rids: |
4794 | + return {} |
4795 | + for rid in rids: |
4796 | + for unit in related_units(rid): |
4797 | + api_server = relation_get('glance-api-server', |
4798 | + rid=rid, unit=unit) |
4799 | + if api_server: |
4800 | + return {'glance_api_servers': api_server} |
4801 | + log('ImageService context is incomplete. ' |
4802 | + 'Missing required relation data.') |
4803 | + return {} |
4804 | + |
4805 | + |
4806 | +class ApacheSSLContext(OSContextGenerator): |
4807 | + """ |
4808 | + Generates a context for an apache vhost configuration that configures |
4809 | + HTTPS reverse proxying for one or many endpoints. Generated context |
4810 | + looks something like: |
4811 | + { |
4812 | + 'namespace': 'cinder', |
4813 | + 'private_address': 'iscsi.mycinderhost.com', |
4814 | + 'endpoints': [(8776, 8766), (8777, 8767)] |
4815 | + } |
4816 | + |
4817 | + The endpoints list consists of a tuples mapping external ports |
4818 | + to internal ports. |
4819 | + """ |
4820 | + interfaces = ['https'] |
4821 | + |
4822 | + # charms should inherit this context and set external ports |
4823 | + # and service namespace accordingly. |
4824 | + external_ports = [] |
4825 | + service_namespace = None |
4826 | + |
4827 | + def enable_modules(self): |
4828 | + cmd = ['a2enmod', 'ssl', 'proxy', 'proxy_http'] |
4829 | + check_call(cmd) |
4830 | + |
4831 | + def configure_cert(self): |
4832 | + if not os.path.isdir('/etc/apache2/ssl'): |
4833 | + os.mkdir('/etc/apache2/ssl') |
4834 | + ssl_dir = os.path.join('/etc/apache2/ssl/', self.service_namespace) |
4835 | + if not os.path.isdir(ssl_dir): |
4836 | + os.mkdir(ssl_dir) |
4837 | + cert, key = get_cert() |
4838 | + with open(os.path.join(ssl_dir, 'cert'), 'w') as cert_out: |
4839 | + cert_out.write(b64decode(cert)) |
4840 | + with open(os.path.join(ssl_dir, 'key'), 'w') as key_out: |
4841 | + key_out.write(b64decode(key)) |
4842 | + ca_cert = get_ca_cert() |
4843 | + if ca_cert: |
4844 | + with open(CA_CERT_PATH, 'w') as ca_out: |
4845 | + ca_out.write(b64decode(ca_cert)) |
4846 | + |
4847 | + def __call__(self): |
4848 | + if isinstance(self.external_ports, basestring): |
4849 | + self.external_ports = [self.external_ports] |
4850 | + if (not self.external_ports or not https()): |
4851 | + return {} |
4852 | + |
4853 | + self.configure_cert() |
4854 | + self.enable_modules() |
4855 | + |
4856 | + ctxt = { |
4857 | + 'namespace': self.service_namespace, |
4858 | + 'private_address': unit_get('private-address'), |
4859 | + 'endpoints': [] |
4860 | + } |
4861 | + for ext_port in self.external_ports: |
4862 | + if peer_units() or is_clustered(): |
4863 | + int_port = determine_haproxy_port(ext_port) |
4864 | + else: |
4865 | + int_port = determine_api_port(ext_port) |
4866 | + portmap = (int(ext_port), int(int_port)) |
4867 | + ctxt['endpoints'].append(portmap) |
4868 | + return ctxt |
4869 | |
4870 | === added directory 'lib/charm-helpers/charmhelpers/contrib/openstack/templates' |
4871 | === added file 'lib/charm-helpers/charmhelpers/contrib/openstack/templates/__init__.py' |
4872 | --- lib/charm-helpers/charmhelpers/contrib/openstack/templates/__init__.py 1970-01-01 00:00:00 +0000 |
4873 | +++ lib/charm-helpers/charmhelpers/contrib/openstack/templates/__init__.py 2013-09-26 06:24:47 +0000 |
4874 | @@ -0,0 +1,2 @@ |
4875 | +# dummy __init__.py to fool syncer into thinking this is a syncable python |
4876 | +# module |
4877 | |
4878 | === added file 'lib/charm-helpers/charmhelpers/contrib/openstack/templates/ceph.conf' |
4879 | --- lib/charm-helpers/charmhelpers/contrib/openstack/templates/ceph.conf 1970-01-01 00:00:00 +0000 |
4880 | +++ lib/charm-helpers/charmhelpers/contrib/openstack/templates/ceph.conf 2013-09-26 06:24:47 +0000 |
4881 | @@ -0,0 +1,11 @@ |
4882 | +############################################################################### |
4883 | +# [ WARNING ] |
4884 | +# cinder configuration file maintained by Juju |
4885 | +# local changes may be overwritten. |
4886 | +############################################################################### |
4887 | +{% if auth -%} |
4888 | +[global] |
4889 | + auth_supported = {{ auth }} |
4890 | + keyring = /etc/ceph/$cluster.$name.keyring |
4891 | + mon host = {{ mon_hosts }} |
4892 | +{% endif -%} |
4893 | |
4894 | === added file 'lib/charm-helpers/charmhelpers/contrib/openstack/templates/haproxy.cfg' |
4895 | --- lib/charm-helpers/charmhelpers/contrib/openstack/templates/haproxy.cfg 1970-01-01 00:00:00 +0000 |
4896 | +++ lib/charm-helpers/charmhelpers/contrib/openstack/templates/haproxy.cfg 2013-09-26 06:24:47 +0000 |
4897 | @@ -0,0 +1,37 @@ |
4898 | +global |
4899 | + log 127.0.0.1 local0 |
4900 | + log 127.0.0.1 local1 notice |
4901 | + maxconn 20000 |
4902 | + user haproxy |
4903 | + group haproxy |
4904 | + spread-checks 0 |
4905 | + |
4906 | +defaults |
4907 | + log global |
4908 | + mode http |
4909 | + option httplog |
4910 | + option dontlognull |
4911 | + retries 3 |
4912 | + timeout queue 1000 |
4913 | + timeout connect 1000 |
4914 | + timeout client 30000 |
4915 | + timeout server 30000 |
4916 | + |
4917 | +listen stats :8888 |
4918 | + mode http |
4919 | + stats enable |
4920 | + stats hide-version |
4921 | + stats realm Haproxy\ Statistics |
4922 | + stats uri / |
4923 | + stats auth admin:password |
4924 | + |
4925 | +{% if units -%} |
4926 | +{% for service, ports in service_ports.iteritems() -%} |
4927 | +listen {{ service }} 0.0.0.0:{{ ports[0] }} |
4928 | + balance roundrobin |
4929 | + option tcplog |
4930 | + {% for unit, address in units.iteritems() -%} |
4931 | + server {{ unit }} {{ address }}:{{ ports[1] }} check |
4932 | + {% endfor %} |
4933 | +{% endfor -%} |
4934 | +{% endif -%} |
4935 | |
4936 | === added file 'lib/charm-helpers/charmhelpers/contrib/openstack/templates/openstack_https_frontend' |
4937 | --- lib/charm-helpers/charmhelpers/contrib/openstack/templates/openstack_https_frontend 1970-01-01 00:00:00 +0000 |
4938 | +++ lib/charm-helpers/charmhelpers/contrib/openstack/templates/openstack_https_frontend 2013-09-26 06:24:47 +0000 |
4939 | @@ -0,0 +1,23 @@ |
4940 | +{% if endpoints -%} |
4941 | +{% for ext, int in endpoints -%} |
4942 | +Listen {{ ext }} |
4943 | +NameVirtualHost *:{{ ext }} |
4944 | +<VirtualHost *:{{ ext }}> |
4945 | + ServerName {{ private_address }} |
4946 | + SSLEngine on |
4947 | + SSLCertificateFile /etc/apache2/ssl/{{ namespace }}/cert |
4948 | + SSLCertificateKeyFile /etc/apache2/ssl/{{ namespace }}/key |
4949 | + ProxyPass / http://localhost:{{ int }}/ |
4950 | + ProxyPassReverse / http://localhost:{{ int }}/ |
4951 | + ProxyPreserveHost on |
4952 | +</VirtualHost> |
4953 | +<Proxy *> |
4954 | + Order deny,allow |
4955 | + Allow from all |
4956 | +</Proxy> |
4957 | +<Location /> |
4958 | + Order allow,deny |
4959 | + Allow from all |
4960 | +</Location> |
4961 | +{% endfor -%} |
4962 | +{% endif -%} |
4963 | |
4964 | === added file 'lib/charm-helpers/charmhelpers/contrib/openstack/templating.py' |
4965 | --- lib/charm-helpers/charmhelpers/contrib/openstack/templating.py 1970-01-01 00:00:00 +0000 |
4966 | +++ lib/charm-helpers/charmhelpers/contrib/openstack/templating.py 2013-09-26 06:24:47 +0000 |
4967 | @@ -0,0 +1,261 @@ |
4968 | +import os |
4969 | + |
4970 | +from charmhelpers.fetch import apt_install |
4971 | + |
4972 | +from charmhelpers.core.hookenv import ( |
4973 | + log, |
4974 | + ERROR, |
4975 | + INFO |
4976 | +) |
4977 | + |
4978 | +from charmhelpers.contrib.openstack.utils import OPENSTACK_CODENAMES |
4979 | + |
4980 | +try: |
4981 | + from jinja2 import FileSystemLoader, ChoiceLoader, Environment |
4982 | +except ImportError: |
4983 | + # python-jinja2 may not be installed yet, or we're running unittests. |
4984 | + FileSystemLoader = ChoiceLoader = Environment = None |
4985 | + |
4986 | + |
4987 | +class OSConfigException(Exception): |
4988 | + pass |
4989 | + |
4990 | + |
4991 | +def get_loader(templates_dir, os_release): |
4992 | + """ |
4993 | + Create a jinja2.ChoiceLoader containing template dirs up to |
4994 | + and including os_release. If directory template directory |
4995 | + is missing at templates_dir, it will be omitted from the loader. |
4996 | + templates_dir is added to the bottom of the search list as a base |
4997 | + loading dir. |
4998 | + |
4999 | + A charm may also ship a templates dir with this module |
5000 | + and it will be appended to the bottom of the search list, eg: |
The diff has been truncated for viewing.
LGTM +1, Thanks for the rewrite. You might want to consider linking to various appropriate sections of the docs in case the README methods become out of date, https:/ /juju.ubuntu. com/docs/ charms- config. html#config- deployment for example.