Merge lp:~opnfv-team/charms/trusty/percona-cluster/power8 into lp:charms/trusty/percona-cluster
- Trusty Tahr (14.04)
- power8
- Merge into trunk
Status: | Needs review |
---|---|
Proposed branch: | lp:~opnfv-team/charms/trusty/percona-cluster/power8 |
Merge into: | lp:charms/trusty/percona-cluster |
Diff against target: |
14145 lines (+13604/-0) (has conflicts) 89 files modified
.coveragerc (+6/-0) .gitignore (+10/-0) .gitreview (+4/-0) .testr.conf (+8/-0) Makefile (+29/-0) README.md (+71/-0) actions.yaml (+20/-0) actions/actions.py (+99/-0) charm-helpers-hooks.yaml (+12/-0) charm-helpers-tests.yaml (+6/-0) charmhelpers/__init__.py (+38/-0) charmhelpers/cli/__init__.py (+191/-0) charmhelpers/cli/benchmark.py (+36/-0) charmhelpers/cli/commands.py (+32/-0) charmhelpers/cli/hookenv.py (+23/-0) charmhelpers/cli/host.py (+31/-0) charmhelpers/cli/unitdata.py (+39/-0) charmhelpers/contrib/__init__.py (+15/-0) charmhelpers/contrib/charmsupport/__init__.py (+15/-0) charmhelpers/contrib/charmsupport/nrpe.py (+398/-0) charmhelpers/contrib/charmsupport/volumes.py (+175/-0) charmhelpers/contrib/database/mysql.py (+415/-0) charmhelpers/contrib/hahelpers/__init__.py (+15/-0) charmhelpers/contrib/hahelpers/cluster.py (+316/-0) charmhelpers/contrib/network/__init__.py (+15/-0) charmhelpers/contrib/network/ip.py (+458/-0) charmhelpers/contrib/peerstorage/__init__.py (+269/-0) charmhelpers/core/__init__.py (+15/-0) charmhelpers/core/decorators.py (+57/-0) charmhelpers/core/files.py (+45/-0) charmhelpers/core/fstab.py (+134/-0) charmhelpers/core/hookenv.py (+978/-0) charmhelpers/core/host.py (+659/-0) charmhelpers/core/hugepage.py (+71/-0) charmhelpers/core/kernel.py (+68/-0) charmhelpers/core/services/__init__.py (+18/-0) charmhelpers/core/services/base.py (+353/-0) charmhelpers/core/services/helpers.py (+292/-0) charmhelpers/core/strutils.py (+72/-0) charmhelpers/core/sysctl.py (+56/-0) charmhelpers/core/templating.py (+81/-0) charmhelpers/core/unitdata.py (+521/-0) charmhelpers/fetch/__init__.py (+464/-0) charmhelpers/fetch/archiveurl.py (+167/-0) charmhelpers/fetch/bzrurl.py (+68/-0) charmhelpers/fetch/giturl.py (+68/-0) charmhelpers/payload/__init__.py (+17/-0) charmhelpers/payload/execd.py (+66/-0) config.yaml (+154/-0) copyright (+29/-0) hooks/install (+20/-0) hooks/percona_hooks.py (+644/-0) hooks/percona_utils.py (+419/-0) keys/repo.percona.com (+30/-0) metadata.yaml (+26/-0) ocf/percona/mysql_monitor (+636/-0) requirements.txt (+12/-0) revision (+1/-0) setup.cfg (+6/-0) templates/my.cnf (+90/-0) templates/mysqld.cnf (+116/-0) test-requirements.txt (+8/-0) tests/10-deploy_test.py (+29/-0) tests/20-broken-mysqld.py (+38/-0) tests/30-kill-9-mysqld.py (+38/-0) tests/31-test-pause-and-resume.py (+38/-0) tests/40-test-bootstrap-single.py (+17/-0) tests/41-test-bootstrap-multi-notmin.py (+41/-0) tests/42-test-bootstrap-multi-min.py (+43/-0) tests/README (+113/-0) tests/basic_deployment.py (+193/-0) tests/charmhelpers/__init__.py (+38/-0) tests/charmhelpers/contrib/__init__.py (+15/-0) tests/charmhelpers/contrib/amulet/__init__.py (+15/-0) tests/charmhelpers/contrib/amulet/deployment.py (+95/-0) tests/charmhelpers/contrib/amulet/utils.py (+818/-0) tests/charmhelpers/contrib/openstack/__init__.py (+15/-0) tests/charmhelpers/contrib/openstack/amulet/__init__.py (+15/-0) tests/charmhelpers/contrib/openstack/amulet/deployment.py (+301/-0) tests/charmhelpers/contrib/openstack/amulet/utils.py (+985/-0) tests/charmhelpers/core/__init__.py (+15/-0) tests/charmhelpers/core/hookenv.py (+978/-0) tests/setup/00-setup (+18/-0) tests/tests.yaml (+21/-0) tox.ini (+30/-0) unit_tests/__init__.py (+3/-0) unit_tests/test_percona_hooks.py (+131/-0) unit_tests/test_percona_utils.py (+233/-0) unit_tests/test_utils.py (+121/-0) Conflict adding file .coveragerc. Moved existing file to .coveragerc.moved. Conflict adding file .gitignore. Moved existing file to .gitignore.moved. Conflict adding file .gitreview. Moved existing file to .gitreview.moved. Conflict adding file Makefile. Moved existing file to Makefile.moved. Conflict adding file README.md. Moved existing file to README.md.moved. Conflict adding file actions. Moved existing file to actions.moved. Conflict adding file actions.yaml. Moved existing file to actions.yaml.moved. Conflict adding file charm-helpers-hooks.yaml. Moved existing file to charm-helpers-hooks.yaml.moved. Conflict adding file charm-helpers-tests.yaml. Moved existing file to charm-helpers-tests.yaml.moved. Conflict adding file charmhelpers. Moved existing file to charmhelpers.moved. Conflict adding file config.yaml. Moved existing file to config.yaml.moved. Conflict adding file copyright. Moved existing file to copyright.moved. Conflict adding file hooks. Moved existing file to hooks.moved. Conflict adding file keys. Moved existing file to keys.moved. Conflict adding file metadata.yaml. Moved existing file to metadata.yaml.moved. Conflict adding file ocf. Moved existing file to ocf.moved. Conflict adding file revision. Moved existing file to revision.moved. Conflict adding file setup.cfg. Moved existing file to setup.cfg.moved. Conflict adding file templates. Moved existing file to templates.moved. Conflict adding file tests. Moved existing file to tests.moved. Conflict adding file unit_tests. Moved existing file to unit_tests.moved. |
To merge this branch: | bzr merge lp:~opnfv-team/charms/trusty/percona-cluster/power8 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
David Ames | Pending | ||
OpenStack Charmers | Pending | ||
Review via email: mp+288722@code.launchpad.net |
Commit message
Description of the change
this merge proposal is from IBM where percona-cluster charm was failing to install on power8 system. It seems there was no root-password was set during install and even config option did not help. But after doing this modification we are able to install this on Power8 system.
Unmerged revisions
- 90. By Narinder Gupta
-
modified to include the power8 changes.
- 89. By Jill Rouleau
-
Add backup action
Add a new action to backup the pxc database on a unit.
Backups by default are stored in /opt/backups/mysql and can
optionally be compressed and done as incremental backups.Change-Id: I5c6ab9fd8be7cb
6cdb2a26e849ec0 b22d8d4f9a6 - 88. By James Page
-
Add tox support for check/gate
This charm was missed pre-migration to git/gerrit.
Add support for executing pep8 and unit tests using tox.
Change-Id: I5518e870c572cc
c292d6fe4e9b7c9 10c7f3f0260 - 87. By uosci-testing-bot
-
Adapt imports and metadata for github move
- 86. By James Page
-
Add gitreview prior to migration to openstack
- 85. By Liam Young
-
[thedac, r=gnuoy] Charm helpers sync. Make the service_running status check cover percona-cluster's unique status message.
- 84. By David Ames
-
[jamespage, r=thedac] Fix Bug:1481362 Make datadir dynamic depending on ubuntu version
- 83. By Liam Young
-
Update test combo definitions, remove Vivid deprecated release tests, update bundletester testplan yaml, update tests README.
- 82. By Liam Young
-
[hopem, r=gnuoy]
Improve shared-db rel logging and tidy get_db_host
Closes-Bug: 1512293 - 81. By James Page
-
Update maintainer
Preview Diff
1 | === added file '.coveragerc' |
2 | --- .coveragerc 1970-01-01 00:00:00 +0000 |
3 | +++ .coveragerc 2016-03-10 22:55:31 +0000 |
4 | @@ -0,0 +1,6 @@ |
5 | +[report] |
6 | +# Regexes for lines to exclude from consideration |
7 | +exclude_lines = |
8 | + if __name__ == .__main__.: |
9 | +include= |
10 | + hooks/percona* |
11 | |
12 | === renamed file '.coveragerc' => '.coveragerc.moved' |
13 | === added file '.gitignore' |
14 | --- .gitignore 1970-01-01 00:00:00 +0000 |
15 | +++ .gitignore 2016-03-10 22:55:31 +0000 |
16 | @@ -0,0 +1,10 @@ |
17 | +bin |
18 | +.coverage |
19 | +.pydevproject |
20 | +.project |
21 | +*.pyc |
22 | +*.pyo |
23 | +__pycache__ |
24 | +*.sw[nop] |
25 | +.testrepository |
26 | +.tox |
27 | |
28 | === renamed file '.gitignore' => '.gitignore.moved' |
29 | === added file '.gitreview' |
30 | --- .gitreview 1970-01-01 00:00:00 +0000 |
31 | +++ .gitreview 2016-03-10 22:55:31 +0000 |
32 | @@ -0,0 +1,4 @@ |
33 | +[gerrit] |
34 | +host=review.openstack.org |
35 | +port=29418 |
36 | +project=openstack/charm-percona-cluster.git |
37 | |
38 | === renamed file '.gitreview' => '.gitreview.moved' |
39 | === added file '.testr.conf' |
40 | --- .testr.conf 1970-01-01 00:00:00 +0000 |
41 | +++ .testr.conf 2016-03-10 22:55:31 +0000 |
42 | @@ -0,0 +1,8 @@ |
43 | +[DEFAULT] |
44 | +test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \ |
45 | + OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} \ |
46 | + OS_TEST_TIMEOUT=${OS_TEST_TIMEOUT:-60} \ |
47 | + ${PYTHON:-python} -m subunit.run discover -t ./ ./unit_tests $LISTOPT $IDOPTION |
48 | + |
49 | +test_id_option=--load-list $IDFILE |
50 | +test_list_option=--list |
51 | |
52 | === added file 'Makefile' |
53 | --- Makefile 1970-01-01 00:00:00 +0000 |
54 | +++ Makefile 2016-03-10 22:55:31 +0000 |
55 | @@ -0,0 +1,29 @@ |
56 | +#!/usr/bin/make |
57 | +PYTHON := /usr/bin/env python |
58 | +export PYTHONPATH := hooks |
59 | + |
60 | +lint: |
61 | + @flake8 --exclude hooks/charmhelpers,tests/charmhelpers \ |
62 | + actions hooks unit_tests tests |
63 | + @charm proof |
64 | + |
65 | +test: |
66 | + @# Bundletester expects unit tests here. |
67 | + @$(PYTHON) /usr/bin/nosetests -v --nologcapture --with-coverage unit_tests |
68 | + |
69 | +functional_test: |
70 | + @echo Starting amulet tests... |
71 | + @tests/setup/00-setup |
72 | + @juju test -v -p AMULET_HTTP_PROXY,AMULET_OS_VIP --timeout 2700 |
73 | + |
74 | +bin/charm_helpers_sync.py: |
75 | + @mkdir -p bin |
76 | + @bzr cat lp:charm-helpers/tools/charm_helpers_sync/charm_helpers_sync.py \ |
77 | + > bin/charm_helpers_sync.py |
78 | + |
79 | +sync: bin/charm_helpers_sync.py |
80 | + @$(PYTHON) bin/charm_helpers_sync.py -c charm-helpers-hooks.yaml |
81 | + @$(PYTHON) bin/charm_helpers_sync.py -c charm-helpers-tests.yaml |
82 | + |
83 | +publish: lint test |
84 | + bzr push lp:charms/trusty/percona-cluster |
85 | |
86 | === renamed file 'Makefile' => 'Makefile.moved' |
87 | === added file 'README.md' |
88 | --- README.md 1970-01-01 00:00:00 +0000 |
89 | +++ README.md 2016-03-10 22:55:31 +0000 |
90 | @@ -0,0 +1,71 @@ |
91 | +Overview |
92 | +======== |
93 | + |
94 | +Percona XtraDB Cluster is a high availability and high scalability solution for |
95 | +MySQL clustering. Percona XtraDB Cluster integrates Percona Server with the |
96 | +Galera library of MySQL high availability solutions in a single product package |
97 | +which enables you to create a cost-effective MySQL cluster. |
98 | + |
99 | +This charm deploys Percona XtraDB Cluster onto Ubuntu. |
100 | + |
101 | +Usage |
102 | +===== |
103 | + |
104 | +WARNING: Its critical that you follow the bootstrap process detailed in this |
105 | +document in order to end up with a running Active/Active Percona Cluster. |
106 | + |
107 | +Proxy Configuration |
108 | +------------------- |
109 | + |
110 | +If you are deploying this charm on MAAS or in an environment without direct |
111 | +access to the internet, you will need to allow access to repo.percona.com |
112 | +as the charm installs packages direct from the Percona respositories. If you |
113 | +are using squid-deb-proxy, follow the steps below: |
114 | + |
115 | + echo "repo.percona.com" | sudo tee /etc/squid-deb-proxy/mirror-dstdomain.acl.d/40-percona |
116 | + sudo service squid-deb-proxy restart |
117 | + |
118 | +Deployment |
119 | +---------- |
120 | + |
121 | +The first service unit deployed acts as the seed node for the rest of the |
122 | +cluster; in order for the cluster to function correctly, the same MySQL passwords |
123 | +must be used across all nodes: |
124 | + |
125 | + cat > percona.yaml << EOF |
126 | + percona-cluster: |
127 | + root-password: my-root-password |
128 | + sst-password: my-sst-password |
129 | + EOF |
130 | + |
131 | +Once you have created this file, you can deploy the first seed unit: |
132 | + |
133 | + juju deploy --config percona.yaml percona-cluster |
134 | + |
135 | +Once this node is full operational, you can add extra units one at a time to the |
136 | +deployment: |
137 | + |
138 | + juju add-unit percona-cluster |
139 | + |
140 | +A minimium cluster size of three units is recommended. |
141 | + |
142 | +In order to access the cluster, use the hacluster charm to provide a single IP |
143 | +address: |
144 | + |
145 | + juju set percona-cluster vip=10.0.3.200 |
146 | + juju deploy hacluster |
147 | + juju add-relation hacluster percona-cluster |
148 | + |
149 | +Clients can then access using the vip provided. This vip will be passed to |
150 | +related services: |
151 | + |
152 | + juju add-relation keystone percona-cluster |
153 | + |
154 | + |
155 | +Limitiations |
156 | +============ |
157 | + |
158 | +Note that Percona XtraDB Cluster is not a 'scale-out' MySQL solution; reads |
159 | +and writes are channelled through a single service unit and synchronously |
160 | +replicated to other nodes in the cluster; reads/writes are as slow as the |
161 | +slowest node you have in your deployment. |
162 | |
163 | === renamed file 'README.md' => 'README.md.moved' |
164 | === added directory 'actions' |
165 | === renamed directory 'actions' => 'actions.moved' |
166 | === added file 'actions.yaml' |
167 | --- actions.yaml 1970-01-01 00:00:00 +0000 |
168 | +++ actions.yaml 2016-03-10 22:55:31 +0000 |
169 | @@ -0,0 +1,20 @@ |
170 | +pause: |
171 | + description: Pause the MySQL service. |
172 | +resume: |
173 | + description: Resume the MySQL service. |
174 | +backup: |
175 | + description: Full database backup |
176 | + params: |
177 | + basedir: |
178 | + type: string |
179 | + default: "/opt/backups/mysql" |
180 | + description: The base directory for backups |
181 | + compress: |
182 | + type: boolean |
183 | + default: false |
184 | + description: Whether or not to compress the backup |
185 | + incremental: |
186 | + type: boolean |
187 | + default: false |
188 | + description: Make an incremental database backup |
189 | + |
190 | |
191 | === renamed file 'actions.yaml' => 'actions.yaml.moved' |
192 | === added file 'actions/actions.py' |
193 | --- actions/actions.py 1970-01-01 00:00:00 +0000 |
194 | +++ actions/actions.py 2016-03-10 22:55:31 +0000 |
195 | @@ -0,0 +1,99 @@ |
196 | +#!/usr/bin/python |
197 | + |
198 | +import os |
199 | +import sys |
200 | +import subprocess |
201 | +import traceback |
202 | +from time import gmtime, strftime |
203 | + |
204 | +from charmhelpers.core.host import service_pause, service_resume |
205 | +from charmhelpers.core.hookenv import ( |
206 | + action_get, |
207 | + action_set, |
208 | + action_fail, |
209 | + status_set, |
210 | + config, |
211 | +) |
212 | + |
213 | +from percona_utils import assess_status |
214 | + |
215 | +MYSQL_SERVICE = "mysql" |
216 | + |
217 | + |
218 | +def pause(args): |
219 | + """Pause the MySQL service. |
220 | + |
221 | + @raises Exception should the service fail to stop. |
222 | + """ |
223 | + if not service_pause(MYSQL_SERVICE): |
224 | + raise Exception("Failed to pause MySQL service.") |
225 | + status_set( |
226 | + "maintenance", |
227 | + "Unit paused - use 'resume' action to resume normal service") |
228 | + |
229 | + |
230 | +def resume(args): |
231 | + """Resume the MySQL service. |
232 | + |
233 | + @raises Exception should the service fail to start.""" |
234 | + if not service_resume(MYSQL_SERVICE): |
235 | + raise Exception("Failed to resume MySQL service.") |
236 | + assess_status() |
237 | + |
238 | + |
239 | +def backup(): |
240 | + basedir = (action_get("basedir")).lower() |
241 | + compress = (action_get("compress")) |
242 | + incremental = (action_get("incremental")) |
243 | + sstpw = config("sst-password") |
244 | + optionlist = [] |
245 | + |
246 | + # innobackupex will not create recursive dirs that do not already exist, |
247 | + # so help it along |
248 | + if not os.path.exists(basedir): |
249 | + os.makedirs(basedir) |
250 | + |
251 | + # Build a list of options to pass to innobackupex |
252 | + if compress is "true": |
253 | + optionlist.append("--compress") |
254 | + |
255 | + if incremental is "true": |
256 | + optionlist.append("--incremental") |
257 | + |
258 | + try: |
259 | + subprocess.check_call( |
260 | + ['innobackupex', '--compact', '--galera-info', '--rsync', |
261 | + basedir, '--user=sstuser', '--password=' + sstpw] + optionlist) |
262 | + action_set({ |
263 | + 'time-completed': (strftime("%Y-%m-%d %H:%M:%S", gmtime())), |
264 | + 'outcome': 'Success'} |
265 | + ) |
266 | + except subprocess.CalledProcessError as e: |
267 | + action_set({ |
268 | + 'time-completed': (strftime("%Y-%m-%d %H:%M:%S", gmtime())), |
269 | + 'output': e.output, |
270 | + 'return-code': e.returncode, |
271 | + 'traceback': traceback.format_exc()}) |
272 | + action_fail("innobackupex failed, you should log on to the unit" |
273 | + "and check the status of the database") |
274 | + |
275 | +# A dictionary of all the defined actions to callables (which take |
276 | +# parsed arguments). |
277 | +ACTIONS = {"pause": pause, "resume": resume, "backup": backup} |
278 | + |
279 | + |
280 | +def main(args): |
281 | + action_name = os.path.basename(args[0]) |
282 | + try: |
283 | + action = ACTIONS[action_name] |
284 | + except KeyError: |
285 | + return "Action %s undefined" % action_name |
286 | + else: |
287 | + try: |
288 | + action(args) |
289 | + except Exception as e: |
290 | + action_fail(str(e)) |
291 | + |
292 | + |
293 | +if __name__ == "__main__": |
294 | + sys.exit(main(sys.argv)) |
295 | |
296 | === added symlink 'actions/charmhelpers' |
297 | === target is u'../charmhelpers' |
298 | === added symlink 'actions/pause' |
299 | === target is u'actions.py' |
300 | === added symlink 'actions/percona_utils.py' |
301 | === target is u'../hooks/percona_utils.py' |
302 | === added symlink 'actions/resume' |
303 | === target is u'actions.py' |
304 | === added file 'charm-helpers-hooks.yaml' |
305 | --- charm-helpers-hooks.yaml 1970-01-01 00:00:00 +0000 |
306 | +++ charm-helpers-hooks.yaml 2016-03-10 22:55:31 +0000 |
307 | @@ -0,0 +1,12 @@ |
308 | +branch: lp:charm-helpers |
309 | +destination: hooks/charmhelpers |
310 | +include: |
311 | + - core |
312 | + - cli |
313 | + - fetch |
314 | + - contrib.hahelpers.cluster |
315 | + - contrib.peerstorage |
316 | + - payload.execd |
317 | + - contrib.network.ip |
318 | + - contrib.database |
319 | + - contrib.charmsupport |
320 | |
321 | === renamed file 'charm-helpers-hooks.yaml' => 'charm-helpers-hooks.yaml.moved' |
322 | === added file 'charm-helpers-tests.yaml' |
323 | --- charm-helpers-tests.yaml 1970-01-01 00:00:00 +0000 |
324 | +++ charm-helpers-tests.yaml 2016-03-10 22:55:31 +0000 |
325 | @@ -0,0 +1,6 @@ |
326 | +branch: lp:charm-helpers |
327 | +destination: tests/charmhelpers |
328 | +include: |
329 | + - contrib.amulet |
330 | + - contrib.openstack.amulet |
331 | + - core.hookenv |
332 | |
333 | === renamed file 'charm-helpers-tests.yaml' => 'charm-helpers-tests.yaml.moved' |
334 | === added directory 'charmhelpers' |
335 | === renamed directory 'charmhelpers' => 'charmhelpers.moved' |
336 | === added file 'charmhelpers/__init__.py' |
337 | --- charmhelpers/__init__.py 1970-01-01 00:00:00 +0000 |
338 | +++ charmhelpers/__init__.py 2016-03-10 22:55:31 +0000 |
339 | @@ -0,0 +1,38 @@ |
340 | +# Copyright 2014-2015 Canonical Limited. |
341 | +# |
342 | +# This file is part of charm-helpers. |
343 | +# |
344 | +# charm-helpers is free software: you can redistribute it and/or modify |
345 | +# it under the terms of the GNU Lesser General Public License version 3 as |
346 | +# published by the Free Software Foundation. |
347 | +# |
348 | +# charm-helpers is distributed in the hope that it will be useful, |
349 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
350 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
351 | +# GNU Lesser General Public License for more details. |
352 | +# |
353 | +# You should have received a copy of the GNU Lesser General Public License |
354 | +# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
355 | + |
356 | +# Bootstrap charm-helpers, installing its dependencies if necessary using |
357 | +# only standard libraries. |
358 | +import subprocess |
359 | +import sys |
360 | + |
361 | +try: |
362 | + import six # flake8: noqa |
363 | +except ImportError: |
364 | + if sys.version_info.major == 2: |
365 | + subprocess.check_call(['apt-get', 'install', '-y', 'python-six']) |
366 | + else: |
367 | + subprocess.check_call(['apt-get', 'install', '-y', 'python3-six']) |
368 | + import six # flake8: noqa |
369 | + |
370 | +try: |
371 | + import yaml # flake8: noqa |
372 | +except ImportError: |
373 | + if sys.version_info.major == 2: |
374 | + subprocess.check_call(['apt-get', 'install', '-y', 'python-yaml']) |
375 | + else: |
376 | + subprocess.check_call(['apt-get', 'install', '-y', 'python3-yaml']) |
377 | + import yaml # flake8: noqa |
378 | |
379 | === added directory 'charmhelpers/cli' |
380 | === added file 'charmhelpers/cli/__init__.py' |
381 | --- charmhelpers/cli/__init__.py 1970-01-01 00:00:00 +0000 |
382 | +++ charmhelpers/cli/__init__.py 2016-03-10 22:55:31 +0000 |
383 | @@ -0,0 +1,191 @@ |
384 | +# Copyright 2014-2015 Canonical Limited. |
385 | +# |
386 | +# This file is part of charm-helpers. |
387 | +# |
388 | +# charm-helpers is free software: you can redistribute it and/or modify |
389 | +# it under the terms of the GNU Lesser General Public License version 3 as |
390 | +# published by the Free Software Foundation. |
391 | +# |
392 | +# charm-helpers is distributed in the hope that it will be useful, |
393 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
394 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
395 | +# GNU Lesser General Public License for more details. |
396 | +# |
397 | +# You should have received a copy of the GNU Lesser General Public License |
398 | +# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
399 | + |
400 | +import inspect |
401 | +import argparse |
402 | +import sys |
403 | + |
404 | +from six.moves import zip |
405 | + |
406 | +import charmhelpers.core.unitdata |
407 | + |
408 | + |
409 | +class OutputFormatter(object): |
410 | + def __init__(self, outfile=sys.stdout): |
411 | + self.formats = ( |
412 | + "raw", |
413 | + "json", |
414 | + "py", |
415 | + "yaml", |
416 | + "csv", |
417 | + "tab", |
418 | + ) |
419 | + self.outfile = outfile |
420 | + |
421 | + def add_arguments(self, argument_parser): |
422 | + formatgroup = argument_parser.add_mutually_exclusive_group() |
423 | + choices = self.supported_formats |
424 | + formatgroup.add_argument("--format", metavar='FMT', |
425 | + help="Select output format for returned data, " |
426 | + "where FMT is one of: {}".format(choices), |
427 | + choices=choices, default='raw') |
428 | + for fmt in self.formats: |
429 | + fmtfunc = getattr(self, fmt) |
430 | + formatgroup.add_argument("-{}".format(fmt[0]), |
431 | + "--{}".format(fmt), action='store_const', |
432 | + const=fmt, dest='format', |
433 | + help=fmtfunc.__doc__) |
434 | + |
435 | + @property |
436 | + def supported_formats(self): |
437 | + return self.formats |
438 | + |
439 | + def raw(self, output): |
440 | + """Output data as raw string (default)""" |
441 | + if isinstance(output, (list, tuple)): |
442 | + output = '\n'.join(map(str, output)) |
443 | + self.outfile.write(str(output)) |
444 | + |
445 | + def py(self, output): |
446 | + """Output data as a nicely-formatted python data structure""" |
447 | + import pprint |
448 | + pprint.pprint(output, stream=self.outfile) |
449 | + |
450 | + def json(self, output): |
451 | + """Output data in JSON format""" |
452 | + import json |
453 | + json.dump(output, self.outfile) |
454 | + |
455 | + def yaml(self, output): |
456 | + """Output data in YAML format""" |
457 | + import yaml |
458 | + yaml.safe_dump(output, self.outfile) |
459 | + |
460 | + def csv(self, output): |
461 | + """Output data as excel-compatible CSV""" |
462 | + import csv |
463 | + csvwriter = csv.writer(self.outfile) |
464 | + csvwriter.writerows(output) |
465 | + |
466 | + def tab(self, output): |
467 | + """Output data in excel-compatible tab-delimited format""" |
468 | + import csv |
469 | + csvwriter = csv.writer(self.outfile, dialect=csv.excel_tab) |
470 | + csvwriter.writerows(output) |
471 | + |
472 | + def format_output(self, output, fmt='raw'): |
473 | + fmtfunc = getattr(self, fmt) |
474 | + fmtfunc(output) |
475 | + |
476 | + |
477 | +class CommandLine(object): |
478 | + argument_parser = None |
479 | + subparsers = None |
480 | + formatter = None |
481 | + exit_code = 0 |
482 | + |
483 | + def __init__(self): |
484 | + if not self.argument_parser: |
485 | + self.argument_parser = argparse.ArgumentParser(description='Perform common charm tasks') |
486 | + if not self.formatter: |
487 | + self.formatter = OutputFormatter() |
488 | + self.formatter.add_arguments(self.argument_parser) |
489 | + if not self.subparsers: |
490 | + self.subparsers = self.argument_parser.add_subparsers(help='Commands') |
491 | + |
492 | + def subcommand(self, command_name=None): |
493 | + """ |
494 | + Decorate a function as a subcommand. Use its arguments as the |
495 | + command-line arguments""" |
496 | + def wrapper(decorated): |
497 | + cmd_name = command_name or decorated.__name__ |
498 | + subparser = self.subparsers.add_parser(cmd_name, |
499 | + description=decorated.__doc__) |
500 | + for args, kwargs in describe_arguments(decorated): |
501 | + subparser.add_argument(*args, **kwargs) |
502 | + subparser.set_defaults(func=decorated) |
503 | + return decorated |
504 | + return wrapper |
505 | + |
506 | + def test_command(self, decorated): |
507 | + """ |
508 | + Subcommand is a boolean test function, so bool return values should be |
509 | + converted to a 0/1 exit code. |
510 | + """ |
511 | + decorated._cli_test_command = True |
512 | + return decorated |
513 | + |
514 | + def no_output(self, decorated): |
515 | + """ |
516 | + Subcommand is not expected to return a value, so don't print a spurious None. |
517 | + """ |
518 | + decorated._cli_no_output = True |
519 | + return decorated |
520 | + |
521 | + def subcommand_builder(self, command_name, description=None): |
522 | + """ |
523 | + Decorate a function that builds a subcommand. Builders should accept a |
524 | + single argument (the subparser instance) and return the function to be |
525 | + run as the command.""" |
526 | + def wrapper(decorated): |
527 | + subparser = self.subparsers.add_parser(command_name) |
528 | + func = decorated(subparser) |
529 | + subparser.set_defaults(func=func) |
530 | + subparser.description = description or func.__doc__ |
531 | + return wrapper |
532 | + |
533 | + def run(self): |
534 | + "Run cli, processing arguments and executing subcommands." |
535 | + arguments = self.argument_parser.parse_args() |
536 | + argspec = inspect.getargspec(arguments.func) |
537 | + vargs = [] |
538 | + for arg in argspec.args: |
539 | + vargs.append(getattr(arguments, arg)) |
540 | + if argspec.varargs: |
541 | + vargs.extend(getattr(arguments, argspec.varargs)) |
542 | + output = arguments.func(*vargs) |
543 | + if getattr(arguments.func, '_cli_test_command', False): |
544 | + self.exit_code = 0 if output else 1 |
545 | + output = '' |
546 | + if getattr(arguments.func, '_cli_no_output', False): |
547 | + output = '' |
548 | + self.formatter.format_output(output, arguments.format) |
549 | + if charmhelpers.core.unitdata._KV: |
550 | + charmhelpers.core.unitdata._KV.flush() |
551 | + |
552 | + |
553 | +cmdline = CommandLine() |
554 | + |
555 | + |
556 | +def describe_arguments(func): |
557 | + """ |
558 | + Analyze a function's signature and return a data structure suitable for |
559 | + passing in as arguments to an argparse parser's add_argument() method.""" |
560 | + |
561 | + argspec = inspect.getargspec(func) |
562 | + # we should probably raise an exception somewhere if func includes **kwargs |
563 | + if argspec.defaults: |
564 | + positional_args = argspec.args[:-len(argspec.defaults)] |
565 | + keyword_names = argspec.args[-len(argspec.defaults):] |
566 | + for arg, default in zip(keyword_names, argspec.defaults): |
567 | + yield ('--{}'.format(arg),), {'default': default} |
568 | + else: |
569 | + positional_args = argspec.args |
570 | + |
571 | + for arg in positional_args: |
572 | + yield (arg,), {} |
573 | + if argspec.varargs: |
574 | + yield (argspec.varargs,), {'nargs': '*'} |
575 | |
576 | === added file 'charmhelpers/cli/benchmark.py' |
577 | --- charmhelpers/cli/benchmark.py 1970-01-01 00:00:00 +0000 |
578 | +++ charmhelpers/cli/benchmark.py 2016-03-10 22:55:31 +0000 |
579 | @@ -0,0 +1,36 @@ |
580 | +# Copyright 2014-2015 Canonical Limited. |
581 | +# |
582 | +# This file is part of charm-helpers. |
583 | +# |
584 | +# charm-helpers is free software: you can redistribute it and/or modify |
585 | +# it under the terms of the GNU Lesser General Public License version 3 as |
586 | +# published by the Free Software Foundation. |
587 | +# |
588 | +# charm-helpers is distributed in the hope that it will be useful, |
589 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
590 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
591 | +# GNU Lesser General Public License for more details. |
592 | +# |
593 | +# You should have received a copy of the GNU Lesser General Public License |
594 | +# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
595 | + |
596 | +from . import cmdline |
597 | +from charmhelpers.contrib.benchmark import Benchmark |
598 | + |
599 | + |
600 | +@cmdline.subcommand(command_name='benchmark-start') |
601 | +def start(): |
602 | + Benchmark.start() |
603 | + |
604 | + |
605 | +@cmdline.subcommand(command_name='benchmark-finish') |
606 | +def finish(): |
607 | + Benchmark.finish() |
608 | + |
609 | + |
610 | +@cmdline.subcommand_builder('benchmark-composite', description="Set the benchmark composite score") |
611 | +def service(subparser): |
612 | + subparser.add_argument("value", help="The composite score.") |
613 | + subparser.add_argument("units", help="The units the composite score represents, i.e., 'reads/sec'.") |
614 | + subparser.add_argument("direction", help="'asc' if a lower score is better, 'desc' if a higher score is better.") |
615 | + return Benchmark.set_composite_score |
616 | |
617 | === added file 'charmhelpers/cli/commands.py' |
618 | --- charmhelpers/cli/commands.py 1970-01-01 00:00:00 +0000 |
619 | +++ charmhelpers/cli/commands.py 2016-03-10 22:55:31 +0000 |
620 | @@ -0,0 +1,32 @@ |
621 | +# Copyright 2014-2015 Canonical Limited. |
622 | +# |
623 | +# This file is part of charm-helpers. |
624 | +# |
625 | +# charm-helpers is free software: you can redistribute it and/or modify |
626 | +# it under the terms of the GNU Lesser General Public License version 3 as |
627 | +# published by the Free Software Foundation. |
628 | +# |
629 | +# charm-helpers is distributed in the hope that it will be useful, |
630 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
631 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
632 | +# GNU Lesser General Public License for more details. |
633 | +# |
634 | +# You should have received a copy of the GNU Lesser General Public License |
635 | +# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
636 | + |
637 | +""" |
638 | +This module loads sub-modules into the python runtime so they can be |
639 | +discovered via the inspect module. In order to prevent flake8 from (rightfully) |
640 | +telling us these are unused modules, throw a ' # noqa' at the end of each import |
641 | +so that the warning is suppressed. |
642 | +""" |
643 | + |
644 | +from . import CommandLine # noqa |
645 | + |
646 | +""" |
647 | +Import the sub-modules which have decorated subcommands to register with chlp. |
648 | +""" |
649 | +from . import host # noqa |
650 | +from . import benchmark # noqa |
651 | +from . import unitdata # noqa |
652 | +from . import hookenv # noqa |
653 | |
654 | === added file 'charmhelpers/cli/hookenv.py' |
655 | --- charmhelpers/cli/hookenv.py 1970-01-01 00:00:00 +0000 |
656 | +++ charmhelpers/cli/hookenv.py 2016-03-10 22:55:31 +0000 |
657 | @@ -0,0 +1,23 @@ |
658 | +# Copyright 2014-2015 Canonical Limited. |
659 | +# |
660 | +# This file is part of charm-helpers. |
661 | +# |
662 | +# charm-helpers is free software: you can redistribute it and/or modify |
663 | +# it under the terms of the GNU Lesser General Public License version 3 as |
664 | +# published by the Free Software Foundation. |
665 | +# |
666 | +# charm-helpers is distributed in the hope that it will be useful, |
667 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
668 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
669 | +# GNU Lesser General Public License for more details. |
670 | +# |
671 | +# You should have received a copy of the GNU Lesser General Public License |
672 | +# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
673 | + |
674 | +from . import cmdline |
675 | +from charmhelpers.core import hookenv |
676 | + |
677 | + |
678 | +cmdline.subcommand('relation-id')(hookenv.relation_id._wrapped) |
679 | +cmdline.subcommand('service-name')(hookenv.service_name) |
680 | +cmdline.subcommand('remote-service-name')(hookenv.remote_service_name._wrapped) |
681 | |
682 | === added file 'charmhelpers/cli/host.py' |
683 | --- charmhelpers/cli/host.py 1970-01-01 00:00:00 +0000 |
684 | +++ charmhelpers/cli/host.py 2016-03-10 22:55:31 +0000 |
685 | @@ -0,0 +1,31 @@ |
686 | +# Copyright 2014-2015 Canonical Limited. |
687 | +# |
688 | +# This file is part of charm-helpers. |
689 | +# |
690 | +# charm-helpers is free software: you can redistribute it and/or modify |
691 | +# it under the terms of the GNU Lesser General Public License version 3 as |
692 | +# published by the Free Software Foundation. |
693 | +# |
694 | +# charm-helpers is distributed in the hope that it will be useful, |
695 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
696 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
697 | +# GNU Lesser General Public License for more details. |
698 | +# |
699 | +# You should have received a copy of the GNU Lesser General Public License |
700 | +# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
701 | + |
702 | +from . import cmdline |
703 | +from charmhelpers.core import host |
704 | + |
705 | + |
706 | +@cmdline.subcommand() |
707 | +def mounts(): |
708 | + "List mounts" |
709 | + return host.mounts() |
710 | + |
711 | + |
712 | +@cmdline.subcommand_builder('service', description="Control system services") |
713 | +def service(subparser): |
714 | + subparser.add_argument("action", help="The action to perform (start, stop, etc...)") |
715 | + subparser.add_argument("service_name", help="Name of the service to control") |
716 | + return host.service |
717 | |
718 | === added file 'charmhelpers/cli/unitdata.py' |
719 | --- charmhelpers/cli/unitdata.py 1970-01-01 00:00:00 +0000 |
720 | +++ charmhelpers/cli/unitdata.py 2016-03-10 22:55:31 +0000 |
721 | @@ -0,0 +1,39 @@ |
722 | +# Copyright 2014-2015 Canonical Limited. |
723 | +# |
724 | +# This file is part of charm-helpers. |
725 | +# |
726 | +# charm-helpers is free software: you can redistribute it and/or modify |
727 | +# it under the terms of the GNU Lesser General Public License version 3 as |
728 | +# published by the Free Software Foundation. |
729 | +# |
730 | +# charm-helpers is distributed in the hope that it will be useful, |
731 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
732 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
733 | +# GNU Lesser General Public License for more details. |
734 | +# |
735 | +# You should have received a copy of the GNU Lesser General Public License |
736 | +# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
737 | + |
738 | +from . import cmdline |
739 | +from charmhelpers.core import unitdata |
740 | + |
741 | + |
742 | +@cmdline.subcommand_builder('unitdata', description="Store and retrieve data") |
743 | +def unitdata_cmd(subparser): |
744 | + nested = subparser.add_subparsers() |
745 | + get_cmd = nested.add_parser('get', help='Retrieve data') |
746 | + get_cmd.add_argument('key', help='Key to retrieve the value of') |
747 | + get_cmd.set_defaults(action='get', value=None) |
748 | + set_cmd = nested.add_parser('set', help='Store data') |
749 | + set_cmd.add_argument('key', help='Key to set') |
750 | + set_cmd.add_argument('value', help='Value to store') |
751 | + set_cmd.set_defaults(action='set') |
752 | + |
753 | + def _unitdata_cmd(action, key, value): |
754 | + if action == 'get': |
755 | + return unitdata.kv().get(key) |
756 | + elif action == 'set': |
757 | + unitdata.kv().set(key, value) |
758 | + unitdata.kv().flush() |
759 | + return '' |
760 | + return _unitdata_cmd |
761 | |
762 | === added directory 'charmhelpers/contrib' |
763 | === added file 'charmhelpers/contrib/__init__.py' |
764 | --- charmhelpers/contrib/__init__.py 1970-01-01 00:00:00 +0000 |
765 | +++ charmhelpers/contrib/__init__.py 2016-03-10 22:55:31 +0000 |
766 | @@ -0,0 +1,15 @@ |
767 | +# Copyright 2014-2015 Canonical Limited. |
768 | +# |
769 | +# This file is part of charm-helpers. |
770 | +# |
771 | +# charm-helpers is free software: you can redistribute it and/or modify |
772 | +# it under the terms of the GNU Lesser General Public License version 3 as |
773 | +# published by the Free Software Foundation. |
774 | +# |
775 | +# charm-helpers is distributed in the hope that it will be useful, |
776 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
777 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
778 | +# GNU Lesser General Public License for more details. |
779 | +# |
780 | +# You should have received a copy of the GNU Lesser General Public License |
781 | +# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
782 | |
783 | === added directory 'charmhelpers/contrib/charmsupport' |
784 | === added file 'charmhelpers/contrib/charmsupport/__init__.py' |
785 | --- charmhelpers/contrib/charmsupport/__init__.py 1970-01-01 00:00:00 +0000 |
786 | +++ charmhelpers/contrib/charmsupport/__init__.py 2016-03-10 22:55:31 +0000 |
787 | @@ -0,0 +1,15 @@ |
788 | +# Copyright 2014-2015 Canonical Limited. |
789 | +# |
790 | +# This file is part of charm-helpers. |
791 | +# |
792 | +# charm-helpers is free software: you can redistribute it and/or modify |
793 | +# it under the terms of the GNU Lesser General Public License version 3 as |
794 | +# published by the Free Software Foundation. |
795 | +# |
796 | +# charm-helpers is distributed in the hope that it will be useful, |
797 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
798 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
799 | +# GNU Lesser General Public License for more details. |
800 | +# |
801 | +# You should have received a copy of the GNU Lesser General Public License |
802 | +# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
803 | |
804 | === added file 'charmhelpers/contrib/charmsupport/nrpe.py' |
805 | --- charmhelpers/contrib/charmsupport/nrpe.py 1970-01-01 00:00:00 +0000 |
806 | +++ charmhelpers/contrib/charmsupport/nrpe.py 2016-03-10 22:55:31 +0000 |
807 | @@ -0,0 +1,398 @@ |
808 | +# Copyright 2014-2015 Canonical Limited. |
809 | +# |
810 | +# This file is part of charm-helpers. |
811 | +# |
812 | +# charm-helpers is free software: you can redistribute it and/or modify |
813 | +# it under the terms of the GNU Lesser General Public License version 3 as |
814 | +# published by the Free Software Foundation. |
815 | +# |
816 | +# charm-helpers is distributed in the hope that it will be useful, |
817 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
818 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
819 | +# GNU Lesser General Public License for more details. |
820 | +# |
821 | +# You should have received a copy of the GNU Lesser General Public License |
822 | +# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
823 | + |
824 | +"""Compatibility with the nrpe-external-master charm""" |
825 | +# Copyright 2012 Canonical Ltd. |
826 | +# |
827 | +# Authors: |
828 | +# Matthew Wedgwood <matthew.wedgwood@canonical.com> |
829 | + |
830 | +import subprocess |
831 | +import pwd |
832 | +import grp |
833 | +import os |
834 | +import glob |
835 | +import shutil |
836 | +import re |
837 | +import shlex |
838 | +import yaml |
839 | + |
840 | +from charmhelpers.core.hookenv import ( |
841 | + config, |
842 | + local_unit, |
843 | + log, |
844 | + relation_ids, |
845 | + relation_set, |
846 | + relations_of_type, |
847 | +) |
848 | + |
849 | +from charmhelpers.core.host import service |
850 | + |
851 | +# This module adds compatibility with the nrpe-external-master and plain nrpe |
852 | +# subordinate charms. To use it in your charm: |
853 | +# |
854 | +# 1. Update metadata.yaml |
855 | +# |
856 | +# provides: |
857 | +# (...) |
858 | +# nrpe-external-master: |
859 | +# interface: nrpe-external-master |
860 | +# scope: container |
861 | +# |
862 | +# and/or |
863 | +# |
864 | +# provides: |
865 | +# (...) |
866 | +# local-monitors: |
867 | +# interface: local-monitors |
868 | +# scope: container |
869 | + |
870 | +# |
871 | +# 2. Add the following to config.yaml |
872 | +# |
873 | +# nagios_context: |
874 | +# default: "juju" |
875 | +# type: string |
876 | +# description: | |
877 | +# Used by the nrpe subordinate charms. |
878 | +# A string that will be prepended to instance name to set the host name |
879 | +# in nagios. So for instance the hostname would be something like: |
880 | +# juju-myservice-0 |
881 | +# If you're running multiple environments with the same services in them |
882 | +# this allows you to differentiate between them. |
883 | +# nagios_servicegroups: |
884 | +# default: "" |
885 | +# type: string |
886 | +# description: | |
887 | +# A comma-separated list of nagios servicegroups. |
888 | +# If left empty, the nagios_context will be used as the servicegroup |
889 | +# |
890 | +# 3. Add custom checks (Nagios plugins) to files/nrpe-external-master |
891 | +# |
892 | +# 4. Update your hooks.py with something like this: |
893 | +# |
894 | +# from charmsupport.nrpe import NRPE |
895 | +# (...) |
896 | +# def update_nrpe_config(): |
897 | +# nrpe_compat = NRPE() |
898 | +# nrpe_compat.add_check( |
899 | +# shortname = "myservice", |
900 | +# description = "Check MyService", |
901 | +# check_cmd = "check_http -w 2 -c 10 http://localhost" |
902 | +# ) |
903 | +# nrpe_compat.add_check( |
904 | +# "myservice_other", |
905 | +# "Check for widget failures", |
906 | +# check_cmd = "/srv/myapp/scripts/widget_check" |
907 | +# ) |
908 | +# nrpe_compat.write() |
909 | +# |
910 | +# def config_changed(): |
911 | +# (...) |
912 | +# update_nrpe_config() |
913 | +# |
914 | +# def nrpe_external_master_relation_changed(): |
915 | +# update_nrpe_config() |
916 | +# |
917 | +# def local_monitors_relation_changed(): |
918 | +# update_nrpe_config() |
919 | +# |
920 | +# 5. ln -s hooks.py nrpe-external-master-relation-changed |
921 | +# ln -s hooks.py local-monitors-relation-changed |
922 | + |
923 | + |
924 | +class CheckException(Exception): |
925 | + pass |
926 | + |
927 | + |
928 | +class Check(object): |
929 | + shortname_re = '[A-Za-z0-9-_]+$' |
930 | + service_template = (""" |
931 | +#--------------------------------------------------- |
932 | +# This file is Juju managed |
933 | +#--------------------------------------------------- |
934 | +define service {{ |
935 | + use active-service |
936 | + host_name {nagios_hostname} |
937 | + service_description {nagios_hostname}[{shortname}] """ |
938 | + """{description} |
939 | + check_command check_nrpe!{command} |
940 | + servicegroups {nagios_servicegroup} |
941 | +}} |
942 | +""") |
943 | + |
944 | + def __init__(self, shortname, description, check_cmd): |
945 | + super(Check, self).__init__() |
946 | + # XXX: could be better to calculate this from the service name |
947 | + if not re.match(self.shortname_re, shortname): |
948 | + raise CheckException("shortname must match {}".format( |
949 | + Check.shortname_re)) |
950 | + self.shortname = shortname |
951 | + self.command = "check_{}".format(shortname) |
952 | + # Note: a set of invalid characters is defined by the |
953 | + # Nagios server config |
954 | + # The default is: illegal_object_name_chars=`~!$%^&*"|'<>?,()= |
955 | + self.description = description |
956 | + self.check_cmd = self._locate_cmd(check_cmd) |
957 | + |
958 | + def _get_check_filename(self): |
959 | + return os.path.join(NRPE.nrpe_confdir, '{}.cfg'.format(self.command)) |
960 | + |
961 | + def _get_service_filename(self, hostname): |
962 | + return os.path.join(NRPE.nagios_exportdir, |
963 | + 'service__{}_{}.cfg'.format(hostname, self.command)) |
964 | + |
965 | + def _locate_cmd(self, check_cmd): |
966 | + search_path = ( |
967 | + '/usr/lib/nagios/plugins', |
968 | + '/usr/local/lib/nagios/plugins', |
969 | + ) |
970 | + parts = shlex.split(check_cmd) |
971 | + for path in search_path: |
972 | + if os.path.exists(os.path.join(path, parts[0])): |
973 | + command = os.path.join(path, parts[0]) |
974 | + if len(parts) > 1: |
975 | + command += " " + " ".join(parts[1:]) |
976 | + return command |
977 | + log('Check command not found: {}'.format(parts[0])) |
978 | + return '' |
979 | + |
980 | + def _remove_service_files(self): |
981 | + if not os.path.exists(NRPE.nagios_exportdir): |
982 | + return |
983 | + for f in os.listdir(NRPE.nagios_exportdir): |
984 | + if f.endswith('_{}.cfg'.format(self.command)): |
985 | + os.remove(os.path.join(NRPE.nagios_exportdir, f)) |
986 | + |
987 | + def remove(self, hostname): |
988 | + nrpe_check_file = self._get_check_filename() |
989 | + if os.path.exists(nrpe_check_file): |
990 | + os.remove(nrpe_check_file) |
991 | + self._remove_service_files() |
992 | + |
993 | + def write(self, nagios_context, hostname, nagios_servicegroups): |
994 | + nrpe_check_file = self._get_check_filename() |
995 | + with open(nrpe_check_file, 'w') as nrpe_check_config: |
996 | + nrpe_check_config.write("# check {}\n".format(self.shortname)) |
997 | + nrpe_check_config.write("command[{}]={}\n".format( |
998 | + self.command, self.check_cmd)) |
999 | + |
1000 | + if not os.path.exists(NRPE.nagios_exportdir): |
1001 | + log('Not writing service config as {} is not accessible'.format( |
1002 | + NRPE.nagios_exportdir)) |
1003 | + else: |
1004 | + self.write_service_config(nagios_context, hostname, |
1005 | + nagios_servicegroups) |
1006 | + |
1007 | + def write_service_config(self, nagios_context, hostname, |
1008 | + nagios_servicegroups): |
1009 | + self._remove_service_files() |
1010 | + |
1011 | + templ_vars = { |
1012 | + 'nagios_hostname': hostname, |
1013 | + 'nagios_servicegroup': nagios_servicegroups, |
1014 | + 'description': self.description, |
1015 | + 'shortname': self.shortname, |
1016 | + 'command': self.command, |
1017 | + } |
1018 | + nrpe_service_text = Check.service_template.format(**templ_vars) |
1019 | + nrpe_service_file = self._get_service_filename(hostname) |
1020 | + with open(nrpe_service_file, 'w') as nrpe_service_config: |
1021 | + nrpe_service_config.write(str(nrpe_service_text)) |
1022 | + |
1023 | + def run(self): |
1024 | + subprocess.call(self.check_cmd) |
1025 | + |
1026 | + |
1027 | +class NRPE(object): |
1028 | + nagios_logdir = '/var/log/nagios' |
1029 | + nagios_exportdir = '/var/lib/nagios/export' |
1030 | + nrpe_confdir = '/etc/nagios/nrpe.d' |
1031 | + |
1032 | + def __init__(self, hostname=None): |
1033 | + super(NRPE, self).__init__() |
1034 | + self.config = config() |
1035 | + self.nagios_context = self.config['nagios_context'] |
1036 | + if 'nagios_servicegroups' in self.config and self.config['nagios_servicegroups']: |
1037 | + self.nagios_servicegroups = self.config['nagios_servicegroups'] |
1038 | + else: |
1039 | + self.nagios_servicegroups = self.nagios_context |
1040 | + self.unit_name = local_unit().replace('/', '-') |
1041 | + if hostname: |
1042 | + self.hostname = hostname |
1043 | + else: |
1044 | + nagios_hostname = get_nagios_hostname() |
1045 | + if nagios_hostname: |
1046 | + self.hostname = nagios_hostname |
1047 | + else: |
1048 | + self.hostname = "{}-{}".format(self.nagios_context, self.unit_name) |
1049 | + self.checks = [] |
1050 | + |
1051 | + def add_check(self, *args, **kwargs): |
1052 | + self.checks.append(Check(*args, **kwargs)) |
1053 | + |
1054 | + def remove_check(self, *args, **kwargs): |
1055 | + if kwargs.get('shortname') is None: |
1056 | + raise ValueError('shortname of check must be specified') |
1057 | + |
1058 | + # Use sensible defaults if they're not specified - these are not |
1059 | + # actually used during removal, but they're required for constructing |
1060 | + # the Check object; check_disk is chosen because it's part of the |
1061 | + # nagios-plugins-basic package. |
1062 | + if kwargs.get('check_cmd') is None: |
1063 | + kwargs['check_cmd'] = 'check_disk' |
1064 | + if kwargs.get('description') is None: |
1065 | + kwargs['description'] = '' |
1066 | + |
1067 | + check = Check(*args, **kwargs) |
1068 | + check.remove(self.hostname) |
1069 | + |
1070 | + def write(self): |
1071 | + try: |
1072 | + nagios_uid = pwd.getpwnam('nagios').pw_uid |
1073 | + nagios_gid = grp.getgrnam('nagios').gr_gid |
1074 | + except: |
1075 | + log("Nagios user not set up, nrpe checks not updated") |
1076 | + return |
1077 | + |
1078 | + if not os.path.exists(NRPE.nagios_logdir): |
1079 | + os.mkdir(NRPE.nagios_logdir) |
1080 | + os.chown(NRPE.nagios_logdir, nagios_uid, nagios_gid) |
1081 | + |
1082 | + nrpe_monitors = {} |
1083 | + monitors = {"monitors": {"remote": {"nrpe": nrpe_monitors}}} |
1084 | + for nrpecheck in self.checks: |
1085 | + nrpecheck.write(self.nagios_context, self.hostname, |
1086 | + self.nagios_servicegroups) |
1087 | + nrpe_monitors[nrpecheck.shortname] = { |
1088 | + "command": nrpecheck.command, |
1089 | + } |
1090 | + |
1091 | + service('restart', 'nagios-nrpe-server') |
1092 | + |
1093 | + monitor_ids = relation_ids("local-monitors") + \ |
1094 | + relation_ids("nrpe-external-master") |
1095 | + for rid in monitor_ids: |
1096 | + relation_set(relation_id=rid, monitors=yaml.dump(monitors)) |
1097 | + |
1098 | + |
1099 | +def get_nagios_hostcontext(relation_name='nrpe-external-master'): |
1100 | + """ |
1101 | + Query relation with nrpe subordinate, return the nagios_host_context |
1102 | + |
1103 | + :param str relation_name: Name of relation nrpe sub joined to |
1104 | + """ |
1105 | + for rel in relations_of_type(relation_name): |
1106 | + if 'nagios_host_context' in rel: |
1107 | + return rel['nagios_host_context'] |
1108 | + |
1109 | + |
1110 | +def get_nagios_hostname(relation_name='nrpe-external-master'): |
1111 | + """ |
1112 | + Query relation with nrpe subordinate, return the nagios_hostname |
1113 | + |
1114 | + :param str relation_name: Name of relation nrpe sub joined to |
1115 | + """ |
1116 | + for rel in relations_of_type(relation_name): |
1117 | + if 'nagios_hostname' in rel: |
1118 | + return rel['nagios_hostname'] |
1119 | + |
1120 | + |
1121 | +def get_nagios_unit_name(relation_name='nrpe-external-master'): |
1122 | + """ |
1123 | + Return the nagios unit name prepended with host_context if needed |
1124 | + |
1125 | + :param str relation_name: Name of relation nrpe sub joined to |
1126 | + """ |
1127 | + host_context = get_nagios_hostcontext(relation_name) |
1128 | + if host_context: |
1129 | + unit = "%s:%s" % (host_context, local_unit()) |
1130 | + else: |
1131 | + unit = local_unit() |
1132 | + return unit |
1133 | + |
1134 | + |
1135 | +def add_init_service_checks(nrpe, services, unit_name): |
1136 | + """ |
1137 | + Add checks for each service in list |
1138 | + |
1139 | + :param NRPE nrpe: NRPE object to add check to |
1140 | + :param list services: List of services to check |
1141 | + :param str unit_name: Unit name to use in check description |
1142 | + """ |
1143 | + for svc in services: |
1144 | + upstart_init = '/etc/init/%s.conf' % svc |
1145 | + sysv_init = '/etc/init.d/%s' % svc |
1146 | + if os.path.exists(upstart_init): |
1147 | + # Don't add a check for these services from neutron-gateway |
1148 | + if svc not in ['ext-port', 'os-charm-phy-nic-mtu']: |
1149 | + nrpe.add_check( |
1150 | + shortname=svc, |
1151 | + description='process check {%s}' % unit_name, |
1152 | + check_cmd='check_upstart_job %s' % svc |
1153 | + ) |
1154 | + elif os.path.exists(sysv_init): |
1155 | + cronpath = '/etc/cron.d/nagios-service-check-%s' % svc |
1156 | + cron_file = ('*/5 * * * * root ' |
1157 | + '/usr/local/lib/nagios/plugins/check_exit_status.pl ' |
1158 | + '-s /etc/init.d/%s status > ' |
1159 | + '/var/lib/nagios/service-check-%s.txt\n' % (svc, |
1160 | + svc) |
1161 | + ) |
1162 | + f = open(cronpath, 'w') |
1163 | + f.write(cron_file) |
1164 | + f.close() |
1165 | + nrpe.add_check( |
1166 | + shortname=svc, |
1167 | + description='process check {%s}' % unit_name, |
1168 | + check_cmd='check_status_file.py -f ' |
1169 | + '/var/lib/nagios/service-check-%s.txt' % svc, |
1170 | + ) |
1171 | + |
1172 | + |
1173 | +def copy_nrpe_checks(): |
1174 | + """ |
1175 | + Copy the nrpe checks into place |
1176 | + |
1177 | + """ |
1178 | + NAGIOS_PLUGINS = '/usr/local/lib/nagios/plugins' |
1179 | + nrpe_files_dir = os.path.join(os.getenv('CHARM_DIR'), 'hooks', |
1180 | + 'charmhelpers', 'contrib', 'openstack', |
1181 | + 'files') |
1182 | + |
1183 | + if not os.path.exists(NAGIOS_PLUGINS): |
1184 | + os.makedirs(NAGIOS_PLUGINS) |
1185 | + for fname in glob.glob(os.path.join(nrpe_files_dir, "check_*")): |
1186 | + if os.path.isfile(fname): |
1187 | + shutil.copy2(fname, |
1188 | + os.path.join(NAGIOS_PLUGINS, os.path.basename(fname))) |
1189 | + |
1190 | + |
1191 | +def add_haproxy_checks(nrpe, unit_name): |
1192 | + """ |
1193 | + Add checks for each service in list |
1194 | + |
1195 | + :param NRPE nrpe: NRPE object to add check to |
1196 | + :param str unit_name: Unit name to use in check description |
1197 | + """ |
1198 | + nrpe.add_check( |
1199 | + shortname='haproxy_servers', |
1200 | + description='Check HAProxy {%s}' % unit_name, |
1201 | + check_cmd='check_haproxy.sh') |
1202 | + nrpe.add_check( |
1203 | + shortname='haproxy_queue', |
1204 | + description='Check HAProxy queue depth {%s}' % unit_name, |
1205 | + check_cmd='check_haproxy_queue_depth.sh') |
1206 | |
1207 | === added file 'charmhelpers/contrib/charmsupport/volumes.py' |
1208 | --- charmhelpers/contrib/charmsupport/volumes.py 1970-01-01 00:00:00 +0000 |
1209 | +++ charmhelpers/contrib/charmsupport/volumes.py 2016-03-10 22:55:31 +0000 |
1210 | @@ -0,0 +1,175 @@ |
1211 | +# Copyright 2014-2015 Canonical Limited. |
1212 | +# |
1213 | +# This file is part of charm-helpers. |
1214 | +# |
1215 | +# charm-helpers is free software: you can redistribute it and/or modify |
1216 | +# it under the terms of the GNU Lesser General Public License version 3 as |
1217 | +# published by the Free Software Foundation. |
1218 | +# |
1219 | +# charm-helpers is distributed in the hope that it will be useful, |
1220 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1221 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1222 | +# GNU Lesser General Public License for more details. |
1223 | +# |
1224 | +# You should have received a copy of the GNU Lesser General Public License |
1225 | +# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
1226 | + |
1227 | +''' |
1228 | +Functions for managing volumes in juju units. One volume is supported per unit. |
1229 | +Subordinates may have their own storage, provided it is on its own partition. |
1230 | + |
1231 | +Configuration stanzas:: |
1232 | + |
1233 | + volume-ephemeral: |
1234 | + type: boolean |
1235 | + default: true |
1236 | + description: > |
1237 | + If false, a volume is mounted as sepecified in "volume-map" |
1238 | + If true, ephemeral storage will be used, meaning that log data |
1239 | + will only exist as long as the machine. YOU HAVE BEEN WARNED. |
1240 | + volume-map: |
1241 | + type: string |
1242 | + default: {} |
1243 | + description: > |
1244 | + YAML map of units to device names, e.g: |
1245 | + "{ rsyslog/0: /dev/vdb, rsyslog/1: /dev/vdb }" |
1246 | + Service units will raise a configure-error if volume-ephemeral |
1247 | + is 'true' and no volume-map value is set. Use 'juju set' to set a |
1248 | + value and 'juju resolved' to complete configuration. |
1249 | + |
1250 | +Usage:: |
1251 | + |
1252 | + from charmsupport.volumes import configure_volume, VolumeConfigurationError |
1253 | + from charmsupport.hookenv import log, ERROR |
1254 | + def post_mount_hook(): |
1255 | + stop_service('myservice') |
1256 | + def post_mount_hook(): |
1257 | + start_service('myservice') |
1258 | + |
1259 | + if __name__ == '__main__': |
1260 | + try: |
1261 | + configure_volume(before_change=pre_mount_hook, |
1262 | + after_change=post_mount_hook) |
1263 | + except VolumeConfigurationError: |
1264 | + log('Storage could not be configured', ERROR) |
1265 | + |
1266 | +''' |
1267 | + |
1268 | +# XXX: Known limitations |
1269 | +# - fstab is neither consulted nor updated |
1270 | + |
1271 | +import os |
1272 | +from charmhelpers.core import hookenv |
1273 | +from charmhelpers.core import host |
1274 | +import yaml |
1275 | + |
1276 | + |
1277 | +MOUNT_BASE = '/srv/juju/volumes' |
1278 | + |
1279 | + |
1280 | +class VolumeConfigurationError(Exception): |
1281 | + '''Volume configuration data is missing or invalid''' |
1282 | + pass |
1283 | + |
1284 | + |
1285 | +def get_config(): |
1286 | + '''Gather and sanity-check volume configuration data''' |
1287 | + volume_config = {} |
1288 | + config = hookenv.config() |
1289 | + |
1290 | + errors = False |
1291 | + |
1292 | + if config.get('volume-ephemeral') in (True, 'True', 'true', 'Yes', 'yes'): |
1293 | + volume_config['ephemeral'] = True |
1294 | + else: |
1295 | + volume_config['ephemeral'] = False |
1296 | + |
1297 | + try: |
1298 | + volume_map = yaml.safe_load(config.get('volume-map', '{}')) |
1299 | + except yaml.YAMLError as e: |
1300 | + hookenv.log("Error parsing YAML volume-map: {}".format(e), |
1301 | + hookenv.ERROR) |
1302 | + errors = True |
1303 | + if volume_map is None: |
1304 | + # probably an empty string |
1305 | + volume_map = {} |
1306 | + elif not isinstance(volume_map, dict): |
1307 | + hookenv.log("Volume-map should be a dictionary, not {}".format( |
1308 | + type(volume_map))) |
1309 | + errors = True |
1310 | + |
1311 | + volume_config['device'] = volume_map.get(os.environ['JUJU_UNIT_NAME']) |
1312 | + if volume_config['device'] and volume_config['ephemeral']: |
1313 | + # asked for ephemeral storage but also defined a volume ID |
1314 | + hookenv.log('A volume is defined for this unit, but ephemeral ' |
1315 | + 'storage was requested', hookenv.ERROR) |
1316 | + errors = True |
1317 | + elif not volume_config['device'] and not volume_config['ephemeral']: |
1318 | + # asked for permanent storage but did not define volume ID |
1319 | + hookenv.log('Ephemeral storage was requested, but there is no volume ' |
1320 | + 'defined for this unit.', hookenv.ERROR) |
1321 | + errors = True |
1322 | + |
1323 | + unit_mount_name = hookenv.local_unit().replace('/', '-') |
1324 | + volume_config['mountpoint'] = os.path.join(MOUNT_BASE, unit_mount_name) |
1325 | + |
1326 | + if errors: |
1327 | + return None |
1328 | + return volume_config |
1329 | + |
1330 | + |
1331 | +def mount_volume(config): |
1332 | + if os.path.exists(config['mountpoint']): |
1333 | + if not os.path.isdir(config['mountpoint']): |
1334 | + hookenv.log('Not a directory: {}'.format(config['mountpoint'])) |
1335 | + raise VolumeConfigurationError() |
1336 | + else: |
1337 | + host.mkdir(config['mountpoint']) |
1338 | + if os.path.ismount(config['mountpoint']): |
1339 | + unmount_volume(config) |
1340 | + if not host.mount(config['device'], config['mountpoint'], persist=True): |
1341 | + raise VolumeConfigurationError() |
1342 | + |
1343 | + |
1344 | +def unmount_volume(config): |
1345 | + if os.path.ismount(config['mountpoint']): |
1346 | + if not host.umount(config['mountpoint'], persist=True): |
1347 | + raise VolumeConfigurationError() |
1348 | + |
1349 | + |
1350 | +def managed_mounts(): |
1351 | + '''List of all mounted managed volumes''' |
1352 | + return filter(lambda mount: mount[0].startswith(MOUNT_BASE), host.mounts()) |
1353 | + |
1354 | + |
1355 | +def configure_volume(before_change=lambda: None, after_change=lambda: None): |
1356 | + '''Set up storage (or don't) according to the charm's volume configuration. |
1357 | + Returns the mount point or "ephemeral". before_change and after_change |
1358 | + are optional functions to be called if the volume configuration changes. |
1359 | + ''' |
1360 | + |
1361 | + config = get_config() |
1362 | + if not config: |
1363 | + hookenv.log('Failed to read volume configuration', hookenv.CRITICAL) |
1364 | + raise VolumeConfigurationError() |
1365 | + |
1366 | + if config['ephemeral']: |
1367 | + if os.path.ismount(config['mountpoint']): |
1368 | + before_change() |
1369 | + unmount_volume(config) |
1370 | + after_change() |
1371 | + return 'ephemeral' |
1372 | + else: |
1373 | + # persistent storage |
1374 | + if os.path.ismount(config['mountpoint']): |
1375 | + mounts = dict(managed_mounts()) |
1376 | + if mounts.get(config['mountpoint']) != config['device']: |
1377 | + before_change() |
1378 | + unmount_volume(config) |
1379 | + mount_volume(config) |
1380 | + after_change() |
1381 | + else: |
1382 | + before_change() |
1383 | + mount_volume(config) |
1384 | + after_change() |
1385 | + return config['mountpoint'] |
1386 | |
1387 | === added directory 'charmhelpers/contrib/database' |
1388 | === added file 'charmhelpers/contrib/database/__init__.py' |
1389 | === added file 'charmhelpers/contrib/database/mysql.py' |
1390 | --- charmhelpers/contrib/database/mysql.py 1970-01-01 00:00:00 +0000 |
1391 | +++ charmhelpers/contrib/database/mysql.py 2016-03-10 22:55:31 +0000 |
1392 | @@ -0,0 +1,415 @@ |
1393 | +"""Helper for working with a MySQL database""" |
1394 | +import json |
1395 | +import re |
1396 | +import sys |
1397 | +import platform |
1398 | +import os |
1399 | +import glob |
1400 | + |
1401 | +# from string import upper |
1402 | + |
1403 | +from charmhelpers.core.host import ( |
1404 | + mkdir, |
1405 | + pwgen, |
1406 | + write_file |
1407 | +) |
1408 | +from charmhelpers.core.hookenv import ( |
1409 | + config as config_get, |
1410 | + relation_get, |
1411 | + related_units, |
1412 | + unit_get, |
1413 | + log, |
1414 | + DEBUG, |
1415 | + INFO, |
1416 | + WARNING, |
1417 | +) |
1418 | +from charmhelpers.fetch import ( |
1419 | + apt_install, |
1420 | + apt_update, |
1421 | + filter_installed_packages, |
1422 | +) |
1423 | +from charmhelpers.contrib.peerstorage import ( |
1424 | + peer_store, |
1425 | + peer_retrieve, |
1426 | +) |
1427 | +from charmhelpers.contrib.network.ip import get_host_ip |
1428 | + |
1429 | +try: |
1430 | + import MySQLdb |
1431 | +except ImportError: |
1432 | + apt_update(fatal=True) |
1433 | + apt_install(filter_installed_packages(['python-mysqldb']), fatal=True) |
1434 | + import MySQLdb |
1435 | + |
1436 | + |
1437 | +class MySQLHelper(object): |
1438 | + |
1439 | + def __init__(self, rpasswdf_template, upasswdf_template, host='localhost', |
1440 | + migrate_passwd_to_peer_relation=True, |
1441 | + delete_ondisk_passwd_file=True): |
1442 | + self.host = host |
1443 | + # Password file path templates |
1444 | + self.root_passwd_file_template = rpasswdf_template |
1445 | + self.user_passwd_file_template = upasswdf_template |
1446 | + |
1447 | + self.migrate_passwd_to_peer_relation = migrate_passwd_to_peer_relation |
1448 | + # If we migrate we have the option to delete local copy of root passwd |
1449 | + self.delete_ondisk_passwd_file = delete_ondisk_passwd_file |
1450 | + |
1451 | + def connect(self, user='root', password=None): |
1452 | + log("Opening db connection for %s@%s" % (user, self.host), level=DEBUG) |
1453 | + if password==None: |
1454 | + self.connection = MySQLdb.connect(user=user, host=self.host) |
1455 | + else: |
1456 | + self.connection = MySQLdb.connect(user=user, host=self.host, |
1457 | + passwd=password) |
1458 | + |
1459 | + def database_exists(self, db_name): |
1460 | + cursor = self.connection.cursor() |
1461 | + try: |
1462 | + cursor.execute("SHOW DATABASES") |
1463 | + databases = [i[0] for i in cursor.fetchall()] |
1464 | + finally: |
1465 | + cursor.close() |
1466 | + |
1467 | + return db_name in databases |
1468 | + |
1469 | + def create_database(self, db_name): |
1470 | + cursor = self.connection.cursor() |
1471 | + try: |
1472 | + cursor.execute("CREATE DATABASE {} CHARACTER SET UTF8" |
1473 | + .format(db_name)) |
1474 | + finally: |
1475 | + cursor.close() |
1476 | + |
1477 | + def grant_exists(self, db_name, db_user, remote_ip): |
1478 | + cursor = self.connection.cursor() |
1479 | + priv_string = "GRANT ALL PRIVILEGES ON `{}`.* " \ |
1480 | + "TO '{}'@'{}'".format(db_name, db_user, remote_ip) |
1481 | + try: |
1482 | + cursor.execute("SHOW GRANTS for '{}'@'{}'".format(db_user, |
1483 | + remote_ip)) |
1484 | + grants = [i[0] for i in cursor.fetchall()] |
1485 | + except MySQLdb.OperationalError: |
1486 | + return False |
1487 | + finally: |
1488 | + cursor.close() |
1489 | + |
1490 | + # TODO: review for different grants |
1491 | + return priv_string in grants |
1492 | + |
1493 | + def create_grant(self, db_name, db_user, remote_ip, password): |
1494 | + cursor = self.connection.cursor() |
1495 | + try: |
1496 | + # TODO: review for different grants |
1497 | + cursor.execute("GRANT ALL PRIVILEGES ON {}.* TO '{}'@'{}' " |
1498 | + "IDENTIFIED BY '{}'".format(db_name, |
1499 | + db_user, |
1500 | + remote_ip, |
1501 | + password)) |
1502 | + finally: |
1503 | + cursor.close() |
1504 | + |
1505 | + def create_admin_grant(self, db_user, remote_ip, password): |
1506 | + cursor = self.connection.cursor() |
1507 | + try: |
1508 | + cursor.execute("GRANT ALL PRIVILEGES ON *.* TO '{}'@'{}' " |
1509 | + "IDENTIFIED BY '{}'".format(db_user, |
1510 | + remote_ip, |
1511 | + password)) |
1512 | + finally: |
1513 | + cursor.close() |
1514 | + |
1515 | + def cleanup_grant(self, db_user, remote_ip): |
1516 | + cursor = self.connection.cursor() |
1517 | + try: |
1518 | + cursor.execute("DROP FROM mysql.user WHERE user='{}' " |
1519 | + "AND HOST='{}'".format(db_user, |
1520 | + remote_ip)) |
1521 | + finally: |
1522 | + cursor.close() |
1523 | + |
1524 | + def execute(self, sql): |
1525 | + """Execute arbitary SQL against the database.""" |
1526 | + cursor = self.connection.cursor() |
1527 | + try: |
1528 | + cursor.execute(sql) |
1529 | + finally: |
1530 | + cursor.close() |
1531 | + |
1532 | + def migrate_passwords_to_peer_relation(self, excludes=None): |
1533 | + """Migrate any passwords storage on disk to cluster peer relation.""" |
1534 | + dirname = os.path.dirname(self.root_passwd_file_template) |
1535 | + path = os.path.join(dirname, '*.passwd') |
1536 | + for f in glob.glob(path): |
1537 | + if excludes and f in excludes: |
1538 | + log("Excluding %s from peer migration" % (f), level=DEBUG) |
1539 | + continue |
1540 | + |
1541 | + key = os.path.basename(f) |
1542 | + with open(f, 'r') as passwd: |
1543 | + _value = passwd.read().strip() |
1544 | + |
1545 | + try: |
1546 | + peer_store(key, _value) |
1547 | + |
1548 | + if self.delete_ondisk_passwd_file: |
1549 | + os.unlink(f) |
1550 | + except ValueError: |
1551 | + # NOTE cluster relation not yet ready - skip for now |
1552 | + pass |
1553 | + |
1554 | + def get_mysql_password_on_disk(self, username=None, password=None): |
1555 | + """Retrieve, generate or store a mysql password for the provided |
1556 | + username on disk.""" |
1557 | + if username: |
1558 | + template = self.user_passwd_file_template |
1559 | + passwd_file = template.format(username) |
1560 | + else: |
1561 | + passwd_file = self.root_passwd_file_template |
1562 | + |
1563 | + _password = None |
1564 | + if os.path.exists(passwd_file): |
1565 | + log("Using existing password file '%s'" % passwd_file, level=DEBUG) |
1566 | + with open(passwd_file, 'r') as passwd: |
1567 | + _password = passwd.read().strip() |
1568 | + else: |
1569 | + log("Generating new password file '%s'" % passwd_file, level=DEBUG) |
1570 | + if not os.path.isdir(os.path.dirname(passwd_file)): |
1571 | + # NOTE: need to ensure this is not mysql root dir (which needs |
1572 | + # to be mysql readable) |
1573 | + mkdir(os.path.dirname(passwd_file), owner='root', group='root', |
1574 | + perms=0o770) |
1575 | + # Force permissions - for some reason the chmod in makedirs |
1576 | + # fails |
1577 | + os.chmod(os.path.dirname(passwd_file), 0o770) |
1578 | + |
1579 | + _password = password or pwgen(length=32) |
1580 | + write_file(passwd_file, _password, owner='root', group='root', |
1581 | + perms=0o660) |
1582 | + |
1583 | + return _password |
1584 | + |
1585 | + def passwd_keys(self, username): |
1586 | + """Generator to return keys used to store passwords in peer store. |
1587 | + |
1588 | + NOTE: we support both legacy and new format to support mysql |
1589 | + charm prior to refactor. This is necessary to avoid LP 1451890. |
1590 | + """ |
1591 | + keys = [] |
1592 | + if username == 'mysql': |
1593 | + log("Bad username '%s'" % (username), level=WARNING) |
1594 | + |
1595 | + if username: |
1596 | + # IMPORTANT: *newer* format must be returned first |
1597 | + keys.append('mysql-%s.passwd' % (username)) |
1598 | + keys.append('%s.passwd' % (username)) |
1599 | + else: |
1600 | + keys.append('mysql.passwd') |
1601 | + |
1602 | + for key in keys: |
1603 | + yield key |
1604 | + |
1605 | + def get_mysql_password(self, username=None, password=None): |
1606 | + """Retrieve, generate or store a mysql password for the provided |
1607 | + username using peer relation cluster.""" |
1608 | + excludes = [] |
1609 | + |
1610 | + # First check peer relation. |
1611 | + try: |
1612 | + for key in self.passwd_keys(username): |
1613 | + _password = peer_retrieve(key) |
1614 | + if _password: |
1615 | + break |
1616 | + |
1617 | + # If root password available don't update peer relation from local |
1618 | + if _password and not username: |
1619 | + excludes.append(self.root_passwd_file_template) |
1620 | + |
1621 | + except ValueError: |
1622 | + # cluster relation is not yet started; use on-disk |
1623 | + _password = None |
1624 | + |
1625 | + # If none available, generate new one |
1626 | + if not _password: |
1627 | + _password = self.get_mysql_password_on_disk(username, password) |
1628 | + |
1629 | + # Put on wire if required |
1630 | + if self.migrate_passwd_to_peer_relation: |
1631 | + self.migrate_passwords_to_peer_relation(excludes=excludes) |
1632 | + |
1633 | + return _password |
1634 | + |
1635 | + def get_mysql_root_password(self, password=None): |
1636 | + """Retrieve or generate mysql root password for service units.""" |
1637 | + return self.get_mysql_password(username=None, password=password) |
1638 | + |
1639 | + def normalize_address(self, hostname): |
1640 | + """Ensure that address returned is an IP address (i.e. not fqdn)""" |
1641 | + if config_get('prefer-ipv6'): |
1642 | + # TODO: add support for ipv6 dns |
1643 | + return hostname |
1644 | + |
1645 | + if hostname != unit_get('private-address'): |
1646 | + return get_host_ip(hostname, fallback=hostname) |
1647 | + |
1648 | + # Otherwise assume localhost |
1649 | + return '127.0.0.1' |
1650 | + |
1651 | + def get_allowed_units(self, database, username, relation_id=None): |
1652 | + """Get list of units with access grants for database with username. |
1653 | + |
1654 | + This is typically used to provide shared-db relations with a list of |
1655 | + which units have been granted access to the given database. |
1656 | + """ |
1657 | + self.connect(password=self.get_mysql_root_password()) |
1658 | + allowed_units = set() |
1659 | + for unit in related_units(relation_id): |
1660 | + settings = relation_get(rid=relation_id, unit=unit) |
1661 | + # First check for setting with prefix, then without |
1662 | + for attr in ["%s_hostname" % (database), 'hostname']: |
1663 | + hosts = settings.get(attr, None) |
1664 | + if hosts: |
1665 | + break |
1666 | + |
1667 | + if hosts: |
1668 | + # hostname can be json-encoded list of hostnames |
1669 | + try: |
1670 | + hosts = json.loads(hosts) |
1671 | + except ValueError: |
1672 | + hosts = [hosts] |
1673 | + else: |
1674 | + hosts = [settings['private-address']] |
1675 | + |
1676 | + if hosts: |
1677 | + for host in hosts: |
1678 | + host = self.normalize_address(host) |
1679 | + if self.grant_exists(database, username, host): |
1680 | + log("Grant exists for host '%s' on db '%s'" % |
1681 | + (host, database), level=DEBUG) |
1682 | + if unit not in allowed_units: |
1683 | + allowed_units.add(unit) |
1684 | + else: |
1685 | + log("Grant does NOT exist for host '%s' on db '%s'" % |
1686 | + (host, database), level=DEBUG) |
1687 | + else: |
1688 | + log("No hosts found for grant check", level=INFO) |
1689 | + |
1690 | + return allowed_units |
1691 | + |
1692 | + def configure_db(self, hostname, database, username, admin=False): |
1693 | + """Configure access to database for username from hostname.""" |
1694 | + self.connect(password=self.get_mysql_root_password()) |
1695 | + if not self.database_exists(database): |
1696 | + self.create_database(database) |
1697 | + |
1698 | + remote_ip = self.normalize_address(hostname) |
1699 | + password = self.get_mysql_password(username) |
1700 | + if not self.grant_exists(database, username, remote_ip): |
1701 | + if not admin: |
1702 | + self.create_grant(database, username, remote_ip, password) |
1703 | + else: |
1704 | + self.create_admin_grant(username, remote_ip, password) |
1705 | + |
1706 | + return password |
1707 | + |
1708 | + |
1709 | +class PerconaClusterHelper(object): |
1710 | + |
1711 | + # Going for the biggest page size to avoid wasted bytes. |
1712 | + # InnoDB page size is 16MB |
1713 | + |
1714 | + DEFAULT_PAGE_SIZE = 16 * 1024 * 1024 |
1715 | + DEFAULT_INNODB_BUFFER_FACTOR = 0.50 |
1716 | + |
1717 | + def human_to_bytes(self, human): |
1718 | + """Convert human readable configuration options to bytes.""" |
1719 | + num_re = re.compile('^[0-9]+$') |
1720 | + if num_re.match(human): |
1721 | + return human |
1722 | + |
1723 | + factors = { |
1724 | + 'K': 1024, |
1725 | + 'M': 1048576, |
1726 | + 'G': 1073741824, |
1727 | + 'T': 1099511627776 |
1728 | + } |
1729 | + modifier = human[-1] |
1730 | + if modifier in factors: |
1731 | + return int(human[:-1]) * factors[modifier] |
1732 | + |
1733 | + if modifier == '%': |
1734 | + total_ram = self.human_to_bytes(self.get_mem_total()) |
1735 | + if self.is_32bit_system() and total_ram > self.sys_mem_limit(): |
1736 | + total_ram = self.sys_mem_limit() |
1737 | + factor = int(human[:-1]) * 0.01 |
1738 | + pctram = total_ram * factor |
1739 | + return int(pctram - (pctram % self.DEFAULT_PAGE_SIZE)) |
1740 | + |
1741 | + raise ValueError("Can only convert K,M,G, or T") |
1742 | + |
1743 | + def is_32bit_system(self): |
1744 | + """Determine whether system is 32 or 64 bit.""" |
1745 | + try: |
1746 | + return sys.maxsize < 2 ** 32 |
1747 | + except OverflowError: |
1748 | + return False |
1749 | + |
1750 | + def sys_mem_limit(self): |
1751 | + """Determine the default memory limit for the current service unit.""" |
1752 | + if platform.machine() in ['armv7l']: |
1753 | + _mem_limit = self.human_to_bytes('2700M') # experimentally determined |
1754 | + else: |
1755 | + # Limit for x86 based 32bit systems |
1756 | + _mem_limit = self.human_to_bytes('4G') |
1757 | + |
1758 | + return _mem_limit |
1759 | + |
1760 | + def get_mem_total(self): |
1761 | + """Calculate the total memory in the current service unit.""" |
1762 | + with open('/proc/meminfo') as meminfo_file: |
1763 | + for line in meminfo_file: |
1764 | + key, mem = line.split(':', 2) |
1765 | + if key == 'MemTotal': |
1766 | + mtot, modifier = mem.strip().split(' ') |
1767 | + return '%s%s' % (mtot, modifier[0].upper()) |
1768 | + |
1769 | + def parse_config(self): |
1770 | + """Parse charm configuration and calculate values for config files.""" |
1771 | + config = config_get() |
1772 | + mysql_config = {} |
1773 | + if 'max-connections' in config: |
1774 | + mysql_config['max_connections'] = config['max-connections'] |
1775 | + |
1776 | + if 'wait-timeout' in config: |
1777 | + mysql_config['wait_timeout'] = config['wait-timeout'] |
1778 | + |
1779 | + if 'innodb-flush-log-at-trx-commit' in config: |
1780 | + mysql_config['innodb_flush_log_at_trx_commit'] = config['innodb-flush-log-at-trx-commit'] |
1781 | + |
1782 | + # Set a sane default key_buffer size |
1783 | + mysql_config['key_buffer'] = self.human_to_bytes('32M') |
1784 | + total_memory = self.human_to_bytes(self.get_mem_total()) |
1785 | + |
1786 | + dataset_bytes = config.get('dataset-size', None) |
1787 | + innodb_buffer_pool_size = config.get('innodb-buffer-pool-size', None) |
1788 | + |
1789 | + if innodb_buffer_pool_size: |
1790 | + innodb_buffer_pool_size = self.human_to_bytes( |
1791 | + innodb_buffer_pool_size) |
1792 | + elif dataset_bytes: |
1793 | + log("Option 'dataset-size' has been deprecated, please use" |
1794 | + "innodb_buffer_pool_size option instead", level="WARN") |
1795 | + innodb_buffer_pool_size = self.human_to_bytes( |
1796 | + dataset_bytes) |
1797 | + else: |
1798 | + innodb_buffer_pool_size = int( |
1799 | + total_memory * self.DEFAULT_INNODB_BUFFER_FACTOR) |
1800 | + |
1801 | + if innodb_buffer_pool_size > total_memory: |
1802 | + log("innodb_buffer_pool_size; {} is greater than system available memory:{}".format( |
1803 | + innodb_buffer_pool_size, |
1804 | + total_memory), level='WARN') |
1805 | + |
1806 | + mysql_config['innodb_buffer_pool_size'] = innodb_buffer_pool_size |
1807 | + return mysql_config |
1808 | |
1809 | === added directory 'charmhelpers/contrib/hahelpers' |
1810 | === added file 'charmhelpers/contrib/hahelpers/__init__.py' |
1811 | --- charmhelpers/contrib/hahelpers/__init__.py 1970-01-01 00:00:00 +0000 |
1812 | +++ charmhelpers/contrib/hahelpers/__init__.py 2016-03-10 22:55:31 +0000 |
1813 | @@ -0,0 +1,15 @@ |
1814 | +# Copyright 2014-2015 Canonical Limited. |
1815 | +# |
1816 | +# This file is part of charm-helpers. |
1817 | +# |
1818 | +# charm-helpers is free software: you can redistribute it and/or modify |
1819 | +# it under the terms of the GNU Lesser General Public License version 3 as |
1820 | +# published by the Free Software Foundation. |
1821 | +# |
1822 | +# charm-helpers is distributed in the hope that it will be useful, |
1823 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1824 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1825 | +# GNU Lesser General Public License for more details. |
1826 | +# |
1827 | +# You should have received a copy of the GNU Lesser General Public License |
1828 | +# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
1829 | |
1830 | === added file 'charmhelpers/contrib/hahelpers/cluster.py' |
1831 | --- charmhelpers/contrib/hahelpers/cluster.py 1970-01-01 00:00:00 +0000 |
1832 | +++ charmhelpers/contrib/hahelpers/cluster.py 2016-03-10 22:55:31 +0000 |
1833 | @@ -0,0 +1,316 @@ |
1834 | +# Copyright 2014-2015 Canonical Limited. |
1835 | +# |
1836 | +# This file is part of charm-helpers. |
1837 | +# |
1838 | +# charm-helpers is free software: you can redistribute it and/or modify |
1839 | +# it under the terms of the GNU Lesser General Public License version 3 as |
1840 | +# published by the Free Software Foundation. |
1841 | +# |
1842 | +# charm-helpers is distributed in the hope that it will be useful, |
1843 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1844 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1845 | +# GNU Lesser General Public License for more details. |
1846 | +# |
1847 | +# You should have received a copy of the GNU Lesser General Public License |
1848 | +# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
1849 | + |
1850 | +# |
1851 | +# Copyright 2012 Canonical Ltd. |
1852 | +# |
1853 | +# Authors: |
1854 | +# James Page <james.page@ubuntu.com> |
1855 | +# Adam Gandelman <adamg@ubuntu.com> |
1856 | +# |
1857 | + |
1858 | +""" |
1859 | +Helpers for clustering and determining "cluster leadership" and other |
1860 | +clustering-related helpers. |
1861 | +""" |
1862 | + |
1863 | +import subprocess |
1864 | +import os |
1865 | + |
1866 | +from socket import gethostname as get_unit_hostname |
1867 | + |
1868 | +import six |
1869 | + |
1870 | +from charmhelpers.core.hookenv import ( |
1871 | + log, |
1872 | + relation_ids, |
1873 | + related_units as relation_list, |
1874 | + relation_get, |
1875 | + config as config_get, |
1876 | + INFO, |
1877 | + ERROR, |
1878 | + WARNING, |
1879 | + unit_get, |
1880 | + is_leader as juju_is_leader |
1881 | +) |
1882 | +from charmhelpers.core.decorators import ( |
1883 | + retry_on_exception, |
1884 | +) |
1885 | +from charmhelpers.core.strutils import ( |
1886 | + bool_from_string, |
1887 | +) |
1888 | + |
1889 | +DC_RESOURCE_NAME = 'DC' |
1890 | + |
1891 | + |
1892 | +class HAIncompleteConfig(Exception): |
1893 | + pass |
1894 | + |
1895 | + |
1896 | +class CRMResourceNotFound(Exception): |
1897 | + pass |
1898 | + |
1899 | + |
1900 | +class CRMDCNotFound(Exception): |
1901 | + pass |
1902 | + |
1903 | + |
1904 | +def is_elected_leader(resource): |
1905 | + """ |
1906 | + Returns True if the charm executing this is the elected cluster leader. |
1907 | + |
1908 | + It relies on two mechanisms to determine leadership: |
1909 | + 1. If juju is sufficiently new and leadership election is supported, |
1910 | + the is_leader command will be used. |
1911 | + 2. If the charm is part of a corosync cluster, call corosync to |
1912 | + determine leadership. |
1913 | + 3. If the charm is not part of a corosync cluster, the leader is |
1914 | + determined as being "the alive unit with the lowest unit numer". In |
1915 | + other words, the oldest surviving unit. |
1916 | + """ |
1917 | + try: |
1918 | + return juju_is_leader() |
1919 | + except NotImplementedError: |
1920 | + log('Juju leadership election feature not enabled' |
1921 | + ', using fallback support', |
1922 | + level=WARNING) |
1923 | + |
1924 | + if is_clustered(): |
1925 | + if not is_crm_leader(resource): |
1926 | + log('Deferring action to CRM leader.', level=INFO) |
1927 | + return False |
1928 | + else: |
1929 | + peers = peer_units() |
1930 | + if peers and not oldest_peer(peers): |
1931 | + log('Deferring action to oldest service unit.', level=INFO) |
1932 | + return False |
1933 | + return True |
1934 | + |
1935 | + |
1936 | +def is_clustered(): |
1937 | + for r_id in (relation_ids('ha') or []): |
1938 | + for unit in (relation_list(r_id) or []): |
1939 | + clustered = relation_get('clustered', |
1940 | + rid=r_id, |
1941 | + unit=unit) |
1942 | + if clustered: |
1943 | + return True |
1944 | + return False |
1945 | + |
1946 | + |
1947 | +def is_crm_dc(): |
1948 | + """ |
1949 | + Determine leadership by querying the pacemaker Designated Controller |
1950 | + """ |
1951 | + cmd = ['crm', 'status'] |
1952 | + try: |
1953 | + status = subprocess.check_output(cmd, stderr=subprocess.STDOUT) |
1954 | + if not isinstance(status, six.text_type): |
1955 | + status = six.text_type(status, "utf-8") |
1956 | + except subprocess.CalledProcessError as ex: |
1957 | + raise CRMDCNotFound(str(ex)) |
1958 | + |
1959 | + current_dc = '' |
1960 | + for line in status.split('\n'): |
1961 | + if line.startswith('Current DC'): |
1962 | + # Current DC: juju-lytrusty-machine-2 (168108163) - partition with quorum |
1963 | + current_dc = line.split(':')[1].split()[0] |
1964 | + if current_dc == get_unit_hostname(): |
1965 | + return True |
1966 | + elif current_dc == 'NONE': |
1967 | + raise CRMDCNotFound('Current DC: NONE') |
1968 | + |
1969 | + return False |
1970 | + |
1971 | + |
1972 | +@retry_on_exception(5, base_delay=2, |
1973 | + exc_type=(CRMResourceNotFound, CRMDCNotFound)) |
1974 | +def is_crm_leader(resource, retry=False): |
1975 | + """ |
1976 | + Returns True if the charm calling this is the elected corosync leader, |
1977 | + as returned by calling the external "crm" command. |
1978 | + |
1979 | + We allow this operation to be retried to avoid the possibility of getting a |
1980 | + false negative. See LP #1396246 for more info. |
1981 | + """ |
1982 | + if resource == DC_RESOURCE_NAME: |
1983 | + return is_crm_dc() |
1984 | + cmd = ['crm', 'resource', 'show', resource] |
1985 | + try: |
1986 | + status = subprocess.check_output(cmd, stderr=subprocess.STDOUT) |
1987 | + if not isinstance(status, six.text_type): |
1988 | + status = six.text_type(status, "utf-8") |
1989 | + except subprocess.CalledProcessError: |
1990 | + status = None |
1991 | + |
1992 | + if status and get_unit_hostname() in status: |
1993 | + return True |
1994 | + |
1995 | + if status and "resource %s is NOT running" % (resource) in status: |
1996 | + raise CRMResourceNotFound("CRM resource %s not found" % (resource)) |
1997 | + |
1998 | + return False |
1999 | + |
2000 | + |
2001 | +def is_leader(resource): |
2002 | + log("is_leader is deprecated. Please consider using is_crm_leader " |
2003 | + "instead.", level=WARNING) |
2004 | + return is_crm_leader(resource) |
2005 | + |
2006 | + |
2007 | +def peer_units(peer_relation="cluster"): |
2008 | + peers = [] |
2009 | + for r_id in (relation_ids(peer_relation) or []): |
2010 | + for unit in (relation_list(r_id) or []): |
2011 | + peers.append(unit) |
2012 | + return peers |
2013 | + |
2014 | + |
2015 | +def peer_ips(peer_relation='cluster', addr_key='private-address'): |
2016 | + '''Return a dict of peers and their private-address''' |
2017 | + peers = {} |
2018 | + for r_id in relation_ids(peer_relation): |
2019 | + for unit in relation_list(r_id): |
2020 | + peers[unit] = relation_get(addr_key, rid=r_id, unit=unit) |
2021 | + return peers |
2022 | + |
2023 | + |
2024 | +def oldest_peer(peers): |
2025 | + """Determines who the oldest peer is by comparing unit numbers.""" |
2026 | + local_unit_no = int(os.getenv('JUJU_UNIT_NAME').split('/')[1]) |
2027 | + for peer in peers: |
2028 | + remote_unit_no = int(peer.split('/')[1]) |
2029 | + if remote_unit_no < local_unit_no: |
2030 | + return False |
2031 | + return True |
2032 | + |
2033 | + |
2034 | +def eligible_leader(resource): |
2035 | + log("eligible_leader is deprecated. Please consider using " |
2036 | + "is_elected_leader instead.", level=WARNING) |
2037 | + return is_elected_leader(resource) |
2038 | + |
2039 | + |
2040 | +def https(): |
2041 | + ''' |
2042 | + Determines whether enough data has been provided in configuration |
2043 | + or relation data to configure HTTPS |
2044 | + . |
2045 | + returns: boolean |
2046 | + ''' |
2047 | + use_https = config_get('use-https') |
2048 | + if use_https and bool_from_string(use_https): |
2049 | + return True |
2050 | + if config_get('ssl_cert') and config_get('ssl_key'): |
2051 | + return True |
2052 | + for r_id in relation_ids('identity-service'): |
2053 | + for unit in relation_list(r_id): |
2054 | + # TODO - needs fixing for new helper as ssl_cert/key suffixes with CN |
2055 | + rel_state = [ |
2056 | + relation_get('https_keystone', rid=r_id, unit=unit), |
2057 | + relation_get('ca_cert', rid=r_id, unit=unit), |
2058 | + ] |
2059 | + # NOTE: works around (LP: #1203241) |
2060 | + if (None not in rel_state) and ('' not in rel_state): |
2061 | + return True |
2062 | + return False |
2063 | + |
2064 | + |
2065 | +def determine_api_port(public_port, singlenode_mode=False): |
2066 | + ''' |
2067 | + Determine correct API server listening port based on |
2068 | + existence of HTTPS reverse proxy and/or haproxy. |
2069 | + |
2070 | + public_port: int: standard public port for given service |
2071 | + |
2072 | + singlenode_mode: boolean: Shuffle ports when only a single unit is present |
2073 | + |
2074 | + returns: int: the correct listening port for the API service |
2075 | + ''' |
2076 | + i = 0 |
2077 | + if singlenode_mode: |
2078 | + i += 1 |
2079 | + elif len(peer_units()) > 0 or is_clustered(): |
2080 | + i += 1 |
2081 | + if https(): |
2082 | + i += 1 |
2083 | + return public_port - (i * 10) |
2084 | + |
2085 | + |
2086 | +def determine_apache_port(public_port, singlenode_mode=False): |
2087 | + ''' |
2088 | + Description: Determine correct apache listening port based on public IP + |
2089 | + state of the cluster. |
2090 | + |
2091 | + public_port: int: standard public port for given service |
2092 | + |
2093 | + singlenode_mode: boolean: Shuffle ports when only a single unit is present |
2094 | + |
2095 | + returns: int: the correct listening port for the HAProxy service |
2096 | + ''' |
2097 | + i = 0 |
2098 | + if singlenode_mode: |
2099 | + i += 1 |
2100 | + elif len(peer_units()) > 0 or is_clustered(): |
2101 | + i += 1 |
2102 | + return public_port - (i * 10) |
2103 | + |
2104 | + |
2105 | +def get_hacluster_config(exclude_keys=None): |
2106 | + ''' |
2107 | + Obtains all relevant configuration from charm configuration required |
2108 | + for initiating a relation to hacluster: |
2109 | + |
2110 | + ha-bindiface, ha-mcastport, vip |
2111 | + |
2112 | + param: exclude_keys: list of setting key(s) to be excluded. |
2113 | + returns: dict: A dict containing settings keyed by setting name. |
2114 | + raises: HAIncompleteConfig if settings are missing. |
2115 | + ''' |
2116 | + settings = ['ha-bindiface', 'ha-mcastport', 'vip'] |
2117 | + conf = {} |
2118 | + for setting in settings: |
2119 | + if exclude_keys and setting in exclude_keys: |
2120 | + continue |
2121 | + |
2122 | + conf[setting] = config_get(setting) |
2123 | + missing = [] |
2124 | + [missing.append(s) for s, v in six.iteritems(conf) if v is None] |
2125 | + if missing: |
2126 | + log('Insufficient config data to configure hacluster.', level=ERROR) |
2127 | + raise HAIncompleteConfig |
2128 | + return conf |
2129 | + |
2130 | + |
2131 | +def canonical_url(configs, vip_setting='vip'): |
2132 | + ''' |
2133 | + Returns the correct HTTP URL to this host given the state of HTTPS |
2134 | + configuration and hacluster. |
2135 | + |
2136 | + :configs : OSTemplateRenderer: A config tempating object to inspect for |
2137 | + a complete https context. |
2138 | + |
2139 | + :vip_setting: str: Setting in charm config that specifies |
2140 | + VIP address. |
2141 | + ''' |
2142 | + scheme = 'http' |
2143 | + if 'https' in configs.complete_contexts(): |
2144 | + scheme = 'https' |
2145 | + if is_clustered(): |
2146 | + addr = config_get(vip_setting) |
2147 | + else: |
2148 | + addr = unit_get('private-address') |
2149 | + return '%s://%s' % (scheme, addr) |
2150 | |
2151 | === added directory 'charmhelpers/contrib/network' |
2152 | === added file 'charmhelpers/contrib/network/__init__.py' |
2153 | --- charmhelpers/contrib/network/__init__.py 1970-01-01 00:00:00 +0000 |
2154 | +++ charmhelpers/contrib/network/__init__.py 2016-03-10 22:55:31 +0000 |
2155 | @@ -0,0 +1,15 @@ |
2156 | +# Copyright 2014-2015 Canonical Limited. |
2157 | +# |
2158 | +# This file is part of charm-helpers. |
2159 | +# |
2160 | +# charm-helpers is free software: you can redistribute it and/or modify |
2161 | +# it under the terms of the GNU Lesser General Public License version 3 as |
2162 | +# published by the Free Software Foundation. |
2163 | +# |
2164 | +# charm-helpers is distributed in the hope that it will be useful, |
2165 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
2166 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
2167 | +# GNU Lesser General Public License for more details. |
2168 | +# |
2169 | +# You should have received a copy of the GNU Lesser General Public License |
2170 | +# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
2171 | |
2172 | === added file 'charmhelpers/contrib/network/ip.py' |
2173 | --- charmhelpers/contrib/network/ip.py 1970-01-01 00:00:00 +0000 |
2174 | +++ charmhelpers/contrib/network/ip.py 2016-03-10 22:55:31 +0000 |
2175 | @@ -0,0 +1,458 @@ |
2176 | +# Copyright 2014-2015 Canonical Limited. |
2177 | +# |
2178 | +# This file is part of charm-helpers. |
2179 | +# |
2180 | +# charm-helpers is free software: you can redistribute it and/or modify |
2181 | +# it under the terms of the GNU Lesser General Public License version 3 as |
2182 | +# published by the Free Software Foundation. |
2183 | +# |
2184 | +# charm-helpers is distributed in the hope that it will be useful, |
2185 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
2186 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
2187 | +# GNU Lesser General Public License for more details. |
2188 | +# |
2189 | +# You should have received a copy of the GNU Lesser General Public License |
2190 | +# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
2191 | + |
2192 | +import glob |
2193 | +import re |
2194 | +import subprocess |
2195 | +import six |
2196 | +import socket |
2197 | + |
2198 | +from functools import partial |
2199 | + |
2200 | +from charmhelpers.core.hookenv import unit_get |
2201 | +from charmhelpers.fetch import apt_install, apt_update |
2202 | +from charmhelpers.core.hookenv import ( |
2203 | + log, |
2204 | + WARNING, |
2205 | +) |
2206 | + |
2207 | +try: |
2208 | + import netifaces |
2209 | +except ImportError: |
2210 | + apt_update(fatal=True) |
2211 | + apt_install('python-netifaces', fatal=True) |
2212 | + import netifaces |
2213 | + |
2214 | +try: |
2215 | + import netaddr |
2216 | +except ImportError: |
2217 | + apt_update(fatal=True) |
2218 | + apt_install('python-netaddr', fatal=True) |
2219 | + import netaddr |
2220 | + |
2221 | + |
2222 | +def _validate_cidr(network): |
2223 | + try: |
2224 | + netaddr.IPNetwork(network) |
2225 | + except (netaddr.core.AddrFormatError, ValueError): |
2226 | + raise ValueError("Network (%s) is not in CIDR presentation format" % |
2227 | + network) |
2228 | + |
2229 | + |
2230 | +def no_ip_found_error_out(network): |
2231 | + errmsg = ("No IP address found in network(s): %s" % network) |
2232 | + raise ValueError(errmsg) |
2233 | + |
2234 | + |
2235 | +def get_address_in_network(network, fallback=None, fatal=False): |
2236 | + """Get an IPv4 or IPv6 address within the network from the host. |
2237 | + |
2238 | + :param network (str): CIDR presentation format. For example, |
2239 | + '192.168.1.0/24'. Supports multiple networks as a space-delimited list. |
2240 | + :param fallback (str): If no address is found, return fallback. |
2241 | + :param fatal (boolean): If no address is found, fallback is not |
2242 | + set and fatal is True then exit(1). |
2243 | + """ |
2244 | + if network is None: |
2245 | + if fallback is not None: |
2246 | + return fallback |
2247 | + |
2248 | + if fatal: |
2249 | + no_ip_found_error_out(network) |
2250 | + else: |
2251 | + return None |
2252 | + |
2253 | + networks = network.split() or [network] |
2254 | + for network in networks: |
2255 | + _validate_cidr(network) |
2256 | + network = netaddr.IPNetwork(network) |
2257 | + for iface in netifaces.interfaces(): |
2258 | + addresses = netifaces.ifaddresses(iface) |
2259 | + if network.version == 4 and netifaces.AF_INET in addresses: |
2260 | + addr = addresses[netifaces.AF_INET][0]['addr'] |
2261 | + netmask = addresses[netifaces.AF_INET][0]['netmask'] |
2262 | + cidr = netaddr.IPNetwork("%s/%s" % (addr, netmask)) |
2263 | + if cidr in network: |
2264 | + return str(cidr.ip) |
2265 | + |
2266 | + if network.version == 6 and netifaces.AF_INET6 in addresses: |
2267 | + for addr in addresses[netifaces.AF_INET6]: |
2268 | + if not addr['addr'].startswith('fe80'): |
2269 | + cidr = netaddr.IPNetwork("%s/%s" % (addr['addr'], |
2270 | + addr['netmask'])) |
2271 | + if cidr in network: |
2272 | + return str(cidr.ip) |
2273 | + |
2274 | + if fallback is not None: |
2275 | + return fallback |
2276 | + |
2277 | + if fatal: |
2278 | + no_ip_found_error_out(network) |
2279 | + |
2280 | + return None |
2281 | + |
2282 | + |
2283 | +def is_ipv6(address): |
2284 | + """Determine whether provided address is IPv6 or not.""" |
2285 | + try: |
2286 | + address = netaddr.IPAddress(address) |
2287 | + except netaddr.AddrFormatError: |
2288 | + # probably a hostname - so not an address at all! |
2289 | + return False |
2290 | + |
2291 | + return address.version == 6 |
2292 | + |
2293 | + |
2294 | +def is_address_in_network(network, address): |
2295 | + """ |
2296 | + Determine whether the provided address is within a network range. |
2297 | + |
2298 | + :param network (str): CIDR presentation format. For example, |
2299 | + '192.168.1.0/24'. |
2300 | + :param address: An individual IPv4 or IPv6 address without a net |
2301 | + mask or subnet prefix. For example, '192.168.1.1'. |
2302 | + :returns boolean: Flag indicating whether address is in network. |
2303 | + """ |
2304 | + try: |
2305 | + network = netaddr.IPNetwork(network) |
2306 | + except (netaddr.core.AddrFormatError, ValueError): |
2307 | + raise ValueError("Network (%s) is not in CIDR presentation format" % |
2308 | + network) |
2309 | + |
2310 | + try: |
2311 | + address = netaddr.IPAddress(address) |
2312 | + except (netaddr.core.AddrFormatError, ValueError): |
2313 | + raise ValueError("Address (%s) is not in correct presentation format" % |
2314 | + address) |
2315 | + |
2316 | + if address in network: |
2317 | + return True |
2318 | + else: |
2319 | + return False |
2320 | + |
2321 | + |
2322 | +def _get_for_address(address, key): |
2323 | + """Retrieve an attribute of or the physical interface that |
2324 | + the IP address provided could be bound to. |
2325 | + |
2326 | + :param address (str): An individual IPv4 or IPv6 address without a net |
2327 | + mask or subnet prefix. For example, '192.168.1.1'. |
2328 | + :param key: 'iface' for the physical interface name or an attribute |
2329 | + of the configured interface, for example 'netmask'. |
2330 | + :returns str: Requested attribute or None if address is not bindable. |
2331 | + """ |
2332 | + address = netaddr.IPAddress(address) |
2333 | + for iface in netifaces.interfaces(): |
2334 | + addresses = netifaces.ifaddresses(iface) |
2335 | + if address.version == 4 and netifaces.AF_INET in addresses: |
2336 | + addr = addresses[netifaces.AF_INET][0]['addr'] |
2337 | + netmask = addresses[netifaces.AF_INET][0]['netmask'] |
2338 | + network = netaddr.IPNetwork("%s/%s" % (addr, netmask)) |
2339 | + cidr = network.cidr |
2340 | + if address in cidr: |
2341 | + if key == 'iface': |
2342 | + return iface |
2343 | + else: |
2344 | + return addresses[netifaces.AF_INET][0][key] |
2345 | + |
2346 | + if address.version == 6 and netifaces.AF_INET6 in addresses: |
2347 | + for addr in addresses[netifaces.AF_INET6]: |
2348 | + if not addr['addr'].startswith('fe80'): |
2349 | + network = netaddr.IPNetwork("%s/%s" % (addr['addr'], |
2350 | + addr['netmask'])) |
2351 | + cidr = network.cidr |
2352 | + if address in cidr: |
2353 | + if key == 'iface': |
2354 | + return iface |
2355 | + elif key == 'netmask' and cidr: |
2356 | + return str(cidr).split('/')[1] |
2357 | + else: |
2358 | + return addr[key] |
2359 | + |
2360 | + return None |
2361 | + |
2362 | + |
2363 | +get_iface_for_address = partial(_get_for_address, key='iface') |
2364 | + |
2365 | + |
2366 | +get_netmask_for_address = partial(_get_for_address, key='netmask') |
2367 | + |
2368 | + |
2369 | +def format_ipv6_addr(address): |
2370 | + """If address is IPv6, wrap it in '[]' otherwise return None. |
2371 | + |
2372 | + This is required by most configuration files when specifying IPv6 |
2373 | + addresses. |
2374 | + """ |
2375 | + if is_ipv6(address): |
2376 | + return "[%s]" % address |
2377 | + |
2378 | + return None |
2379 | + |
2380 | + |
2381 | +def get_iface_addr(iface='eth0', inet_type='AF_INET', inc_aliases=False, |
2382 | + fatal=True, exc_list=None): |
2383 | + """Return the assigned IP address for a given interface, if any.""" |
2384 | + # Extract nic if passed /dev/ethX |
2385 | + if '/' in iface: |
2386 | + iface = iface.split('/')[-1] |
2387 | + |
2388 | + if not exc_list: |
2389 | + exc_list = [] |
2390 | + |
2391 | + try: |
2392 | + inet_num = getattr(netifaces, inet_type) |
2393 | + except AttributeError: |
2394 | + raise Exception("Unknown inet type '%s'" % str(inet_type)) |
2395 | + |
2396 | + interfaces = netifaces.interfaces() |
2397 | + if inc_aliases: |
2398 | + ifaces = [] |
2399 | + for _iface in interfaces: |
2400 | + if iface == _iface or _iface.split(':')[0] == iface: |
2401 | + ifaces.append(_iface) |
2402 | + |
2403 | + if fatal and not ifaces: |
2404 | + raise Exception("Invalid interface '%s'" % iface) |
2405 | + |
2406 | + ifaces.sort() |
2407 | + else: |
2408 | + if iface not in interfaces: |
2409 | + if fatal: |
2410 | + raise Exception("Interface '%s' not found " % (iface)) |
2411 | + else: |
2412 | + return [] |
2413 | + |
2414 | + else: |
2415 | + ifaces = [iface] |
2416 | + |
2417 | + addresses = [] |
2418 | + for netiface in ifaces: |
2419 | + net_info = netifaces.ifaddresses(netiface) |
2420 | + if inet_num in net_info: |
2421 | + for entry in net_info[inet_num]: |
2422 | + if 'addr' in entry and entry['addr'] not in exc_list: |
2423 | + addresses.append(entry['addr']) |
2424 | + |
2425 | + if fatal and not addresses: |
2426 | + raise Exception("Interface '%s' doesn't have any %s addresses." % |
2427 | + (iface, inet_type)) |
2428 | + |
2429 | + return sorted(addresses) |
2430 | + |
2431 | + |
2432 | +get_ipv4_addr = partial(get_iface_addr, inet_type='AF_INET') |
2433 | + |
2434 | + |
2435 | +def get_iface_from_addr(addr): |
2436 | + """Work out on which interface the provided address is configured.""" |
2437 | + for iface in netifaces.interfaces(): |
2438 | + addresses = netifaces.ifaddresses(iface) |
2439 | + for inet_type in addresses: |
2440 | + for _addr in addresses[inet_type]: |
2441 | + _addr = _addr['addr'] |
2442 | + # link local |
2443 | + ll_key = re.compile("(.+)%.*") |
2444 | + raw = re.match(ll_key, _addr) |
2445 | + if raw: |
2446 | + _addr = raw.group(1) |
2447 | + |
2448 | + if _addr == addr: |
2449 | + log("Address '%s' is configured on iface '%s'" % |
2450 | + (addr, iface)) |
2451 | + return iface |
2452 | + |
2453 | + msg = "Unable to infer net iface on which '%s' is configured" % (addr) |
2454 | + raise Exception(msg) |
2455 | + |
2456 | + |
2457 | +def sniff_iface(f): |
2458 | + """Ensure decorated function is called with a value for iface. |
2459 | + |
2460 | + If no iface provided, inject net iface inferred from unit private address. |
2461 | + """ |
2462 | + def iface_sniffer(*args, **kwargs): |
2463 | + if not kwargs.get('iface', None): |
2464 | + kwargs['iface'] = get_iface_from_addr(unit_get('private-address')) |
2465 | + |
2466 | + return f(*args, **kwargs) |
2467 | + |
2468 | + return iface_sniffer |
2469 | + |
2470 | + |
2471 | +@sniff_iface |
2472 | +def get_ipv6_addr(iface=None, inc_aliases=False, fatal=True, exc_list=None, |
2473 | + dynamic_only=True): |
2474 | + """Get assigned IPv6 address for a given interface. |
2475 | + |
2476 | + Returns list of addresses found. If no address found, returns empty list. |
2477 | + |
2478 | + If iface is None, we infer the current primary interface by doing a reverse |
2479 | + lookup on the unit private-address. |
2480 | + |
2481 | + We currently only support scope global IPv6 addresses i.e. non-temporary |
2482 | + addresses. If no global IPv6 address is found, return the first one found |
2483 | + in the ipv6 address list. |
2484 | + """ |
2485 | + addresses = get_iface_addr(iface=iface, inet_type='AF_INET6', |
2486 | + inc_aliases=inc_aliases, fatal=fatal, |
2487 | + exc_list=exc_list) |
2488 | + |
2489 | + if addresses: |
2490 | + global_addrs = [] |
2491 | + for addr in addresses: |
2492 | + key_scope_link_local = re.compile("^fe80::..(.+)%(.+)") |
2493 | + m = re.match(key_scope_link_local, addr) |
2494 | + if m: |
2495 | + eui_64_mac = m.group(1) |
2496 | + iface = m.group(2) |
2497 | + else: |
2498 | + global_addrs.append(addr) |
2499 | + |
2500 | + if global_addrs: |
2501 | + # Make sure any found global addresses are not temporary |
2502 | + cmd = ['ip', 'addr', 'show', iface] |
2503 | + out = subprocess.check_output(cmd).decode('UTF-8') |
2504 | + if dynamic_only: |
2505 | + key = re.compile("inet6 (.+)/[0-9]+ scope global dynamic.*") |
2506 | + else: |
2507 | + key = re.compile("inet6 (.+)/[0-9]+ scope global.*") |
2508 | + |
2509 | + addrs = [] |
2510 | + for line in out.split('\n'): |
2511 | + line = line.strip() |
2512 | + m = re.match(key, line) |
2513 | + if m and 'temporary' not in line: |
2514 | + # Return the first valid address we find |
2515 | + for addr in global_addrs: |
2516 | + if m.group(1) == addr: |
2517 | + if not dynamic_only or \ |
2518 | + m.group(1).endswith(eui_64_mac): |
2519 | + addrs.append(addr) |
2520 | + |
2521 | + if addrs: |
2522 | + return addrs |
2523 | + |
2524 | + if fatal: |
2525 | + raise Exception("Interface '%s' does not have a scope global " |
2526 | + "non-temporary ipv6 address." % iface) |
2527 | + |
2528 | + return [] |
2529 | + |
2530 | + |
2531 | +def get_bridges(vnic_dir='/sys/devices/virtual/net'): |
2532 | + """Return a list of bridges on the system.""" |
2533 | + b_regex = "%s/*/bridge" % vnic_dir |
2534 | + return [x.replace(vnic_dir, '').split('/')[1] for x in glob.glob(b_regex)] |
2535 | + |
2536 | + |
2537 | +def get_bridge_nics(bridge, vnic_dir='/sys/devices/virtual/net'): |
2538 | + """Return a list of nics comprising a given bridge on the system.""" |
2539 | + brif_regex = "%s/%s/brif/*" % (vnic_dir, bridge) |
2540 | + return [x.split('/')[-1] for x in glob.glob(brif_regex)] |
2541 | + |
2542 | + |
2543 | +def is_bridge_member(nic): |
2544 | + """Check if a given nic is a member of a bridge.""" |
2545 | + for bridge in get_bridges(): |
2546 | + if nic in get_bridge_nics(bridge): |
2547 | + return True |
2548 | + |
2549 | + return False |
2550 | + |
2551 | + |
2552 | +def is_ip(address): |
2553 | + """ |
2554 | + Returns True if address is a valid IP address. |
2555 | + """ |
2556 | + try: |
2557 | + # Test to see if already an IPv4 address |
2558 | + socket.inet_aton(address) |
2559 | + return True |
2560 | + except socket.error: |
2561 | + return False |
2562 | + |
2563 | + |
2564 | +def ns_query(address): |
2565 | + try: |
2566 | + import dns.resolver |
2567 | + except ImportError: |
2568 | + apt_install('python-dnspython') |
2569 | + import dns.resolver |
2570 | + |
2571 | + if isinstance(address, dns.name.Name): |
2572 | + rtype = 'PTR' |
2573 | + elif isinstance(address, six.string_types): |
2574 | + rtype = 'A' |
2575 | + else: |
2576 | + return None |
2577 | + |
2578 | + answers = dns.resolver.query(address, rtype) |
2579 | + if answers: |
2580 | + return str(answers[0]) |
2581 | + return None |
2582 | + |
2583 | + |
2584 | +def get_host_ip(hostname, fallback=None): |
2585 | + """ |
2586 | + Resolves the IP for a given hostname, or returns |
2587 | + the input if it is already an IP. |
2588 | + """ |
2589 | + if is_ip(hostname): |
2590 | + return hostname |
2591 | + |
2592 | + ip_addr = ns_query(hostname) |
2593 | + if not ip_addr: |
2594 | + try: |
2595 | + ip_addr = socket.gethostbyname(hostname) |
2596 | + except: |
2597 | + log("Failed to resolve hostname '%s'" % (hostname), |
2598 | + level=WARNING) |
2599 | + return fallback |
2600 | + return ip_addr |
2601 | + |
2602 | + |
2603 | +def get_hostname(address, fqdn=True): |
2604 | + """ |
2605 | + Resolves hostname for given IP, or returns the input |
2606 | + if it is already a hostname. |
2607 | + """ |
2608 | + if is_ip(address): |
2609 | + try: |
2610 | + import dns.reversename |
2611 | + except ImportError: |
2612 | + apt_install("python-dnspython") |
2613 | + import dns.reversename |
2614 | + |
2615 | + rev = dns.reversename.from_address(address) |
2616 | + result = ns_query(rev) |
2617 | + |
2618 | + if not result: |
2619 | + try: |
2620 | + result = socket.gethostbyaddr(address)[0] |
2621 | + except: |
2622 | + return None |
2623 | + else: |
2624 | + result = address |
2625 | + |
2626 | + if fqdn: |
2627 | + # strip trailing . |
2628 | + if result.endswith('.'): |
2629 | + return result[:-1] |
2630 | + else: |
2631 | + return result |
2632 | + else: |
2633 | + return result.split('.')[0] |
2634 | |
2635 | === added directory 'charmhelpers/contrib/peerstorage' |
2636 | === added file 'charmhelpers/contrib/peerstorage/__init__.py' |
2637 | --- charmhelpers/contrib/peerstorage/__init__.py 1970-01-01 00:00:00 +0000 |
2638 | +++ charmhelpers/contrib/peerstorage/__init__.py 2016-03-10 22:55:31 +0000 |
2639 | @@ -0,0 +1,269 @@ |
2640 | +# Copyright 2014-2015 Canonical Limited. |
2641 | +# |
2642 | +# This file is part of charm-helpers. |
2643 | +# |
2644 | +# charm-helpers is free software: you can redistribute it and/or modify |
2645 | +# it under the terms of the GNU Lesser General Public License version 3 as |
2646 | +# published by the Free Software Foundation. |
2647 | +# |
2648 | +# charm-helpers is distributed in the hope that it will be useful, |
2649 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
2650 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
2651 | +# GNU Lesser General Public License for more details. |
2652 | +# |
2653 | +# You should have received a copy of the GNU Lesser General Public License |
2654 | +# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
2655 | + |
2656 | +import json |
2657 | +import six |
2658 | + |
2659 | +from charmhelpers.core.hookenv import relation_id as current_relation_id |
2660 | +from charmhelpers.core.hookenv import ( |
2661 | + is_relation_made, |
2662 | + relation_ids, |
2663 | + relation_get as _relation_get, |
2664 | + local_unit, |
2665 | + relation_set as _relation_set, |
2666 | + leader_get as _leader_get, |
2667 | + leader_set, |
2668 | + is_leader, |
2669 | +) |
2670 | + |
2671 | + |
2672 | +""" |
2673 | +This helper provides functions to support use of a peer relation |
2674 | +for basic key/value storage, with the added benefit that all storage |
2675 | +can be replicated across peer units. |
2676 | + |
2677 | +Requirement to use: |
2678 | + |
2679 | +To use this, the "peer_echo()" method has to be called form the peer |
2680 | +relation's relation-changed hook: |
2681 | + |
2682 | +@hooks.hook("cluster-relation-changed") # Adapt the to your peer relation name |
2683 | +def cluster_relation_changed(): |
2684 | + peer_echo() |
2685 | + |
2686 | +Once this is done, you can use peer storage from anywhere: |
2687 | + |
2688 | +@hooks.hook("some-hook") |
2689 | +def some_hook(): |
2690 | + # You can store and retrieve key/values this way: |
2691 | + if is_relation_made("cluster"): # from charmhelpers.core.hookenv |
2692 | + # There are peers available so we can work with peer storage |
2693 | + peer_store("mykey", "myvalue") |
2694 | + value = peer_retrieve("mykey") |
2695 | + print value |
2696 | + else: |
2697 | + print "No peers joind the relation, cannot share key/values :(" |
2698 | +""" |
2699 | + |
2700 | + |
2701 | +def leader_get(attribute=None, rid=None): |
2702 | + """Wrapper to ensure that settings are migrated from the peer relation. |
2703 | + |
2704 | + This is to support upgrading an environment that does not support |
2705 | + Juju leadership election to one that does. |
2706 | + |
2707 | + If a setting is not extant in the leader-get but is on the relation-get |
2708 | + peer rel, it is migrated and marked as such so that it is not re-migrated. |
2709 | + """ |
2710 | + migration_key = '__leader_get_migrated_settings__' |
2711 | + if not is_leader(): |
2712 | + return _leader_get(attribute=attribute) |
2713 | + |
2714 | + settings_migrated = False |
2715 | + leader_settings = _leader_get(attribute=attribute) |
2716 | + previously_migrated = _leader_get(attribute=migration_key) |
2717 | + |
2718 | + if previously_migrated: |
2719 | + migrated = set(json.loads(previously_migrated)) |
2720 | + else: |
2721 | + migrated = set([]) |
2722 | + |
2723 | + try: |
2724 | + if migration_key in leader_settings: |
2725 | + del leader_settings[migration_key] |
2726 | + except TypeError: |
2727 | + pass |
2728 | + |
2729 | + if attribute: |
2730 | + if attribute in migrated: |
2731 | + return leader_settings |
2732 | + |
2733 | + # If attribute not present in leader db, check if this unit has set |
2734 | + # the attribute in the peer relation |
2735 | + if not leader_settings: |
2736 | + peer_setting = _relation_get(attribute=attribute, unit=local_unit(), |
2737 | + rid=rid) |
2738 | + if peer_setting: |
2739 | + leader_set(settings={attribute: peer_setting}) |
2740 | + leader_settings = peer_setting |
2741 | + |
2742 | + if leader_settings: |
2743 | + settings_migrated = True |
2744 | + migrated.add(attribute) |
2745 | + else: |
2746 | + r_settings = _relation_get(unit=local_unit(), rid=rid) |
2747 | + if r_settings: |
2748 | + for key in set(r_settings.keys()).difference(migrated): |
2749 | + # Leader setting wins |
2750 | + if not leader_settings.get(key): |
2751 | + leader_settings[key] = r_settings[key] |
2752 | + |
2753 | + settings_migrated = True |
2754 | + migrated.add(key) |
2755 | + |
2756 | + if settings_migrated: |
2757 | + leader_set(**leader_settings) |
2758 | + |
2759 | + if migrated and settings_migrated: |
2760 | + migrated = json.dumps(list(migrated)) |
2761 | + leader_set(settings={migration_key: migrated}) |
2762 | + |
2763 | + return leader_settings |
2764 | + |
2765 | + |
2766 | +def relation_set(relation_id=None, relation_settings=None, **kwargs): |
2767 | + """Attempt to use leader-set if supported in the current version of Juju, |
2768 | + otherwise falls back on relation-set. |
2769 | + |
2770 | + Note that we only attempt to use leader-set if the provided relation_id is |
2771 | + a peer relation id or no relation id is provided (in which case we assume |
2772 | + we are within the peer relation context). |
2773 | + """ |
2774 | + try: |
2775 | + if relation_id in relation_ids('cluster'): |
2776 | + return leader_set(settings=relation_settings, **kwargs) |
2777 | + else: |
2778 | + raise NotImplementedError |
2779 | + except NotImplementedError: |
2780 | + return _relation_set(relation_id=relation_id, |
2781 | + relation_settings=relation_settings, **kwargs) |
2782 | + |
2783 | + |
2784 | +def relation_get(attribute=None, unit=None, rid=None): |
2785 | + """Attempt to use leader-get if supported in the current version of Juju, |
2786 | + otherwise falls back on relation-get. |
2787 | + |
2788 | + Note that we only attempt to use leader-get if the provided rid is a peer |
2789 | + relation id or no relation id is provided (in which case we assume we are |
2790 | + within the peer relation context). |
2791 | + """ |
2792 | + try: |
2793 | + if rid in relation_ids('cluster'): |
2794 | + return leader_get(attribute, rid) |
2795 | + else: |
2796 | + raise NotImplementedError |
2797 | + except NotImplementedError: |
2798 | + return _relation_get(attribute=attribute, rid=rid, unit=unit) |
2799 | + |
2800 | + |
2801 | +def peer_retrieve(key, relation_name='cluster'): |
2802 | + """Retrieve a named key from peer relation `relation_name`.""" |
2803 | + cluster_rels = relation_ids(relation_name) |
2804 | + if len(cluster_rels) > 0: |
2805 | + cluster_rid = cluster_rels[0] |
2806 | + return relation_get(attribute=key, rid=cluster_rid, |
2807 | + unit=local_unit()) |
2808 | + else: |
2809 | + raise ValueError('Unable to detect' |
2810 | + 'peer relation {}'.format(relation_name)) |
2811 | + |
2812 | + |
2813 | +def peer_retrieve_by_prefix(prefix, relation_name='cluster', delimiter='_', |
2814 | + inc_list=None, exc_list=None): |
2815 | + """ Retrieve k/v pairs given a prefix and filter using {inc,exc}_list """ |
2816 | + inc_list = inc_list if inc_list else [] |
2817 | + exc_list = exc_list if exc_list else [] |
2818 | + peerdb_settings = peer_retrieve('-', relation_name=relation_name) |
2819 | + matched = {} |
2820 | + if peerdb_settings is None: |
2821 | + return matched |
2822 | + for k, v in peerdb_settings.items(): |
2823 | + full_prefix = prefix + delimiter |
2824 | + if k.startswith(full_prefix): |
2825 | + new_key = k.replace(full_prefix, '') |
2826 | + if new_key in exc_list: |
2827 | + continue |
2828 | + if new_key in inc_list or len(inc_list) == 0: |
2829 | + matched[new_key] = v |
2830 | + return matched |
2831 | + |
2832 | + |
2833 | +def peer_store(key, value, relation_name='cluster'): |
2834 | + """Store the key/value pair on the named peer relation `relation_name`.""" |
2835 | + cluster_rels = relation_ids(relation_name) |
2836 | + if len(cluster_rels) > 0: |
2837 | + cluster_rid = cluster_rels[0] |
2838 | + relation_set(relation_id=cluster_rid, |
2839 | + relation_settings={key: value}) |
2840 | + else: |
2841 | + raise ValueError('Unable to detect ' |
2842 | + 'peer relation {}'.format(relation_name)) |
2843 | + |
2844 | + |
2845 | +def peer_echo(includes=None, force=False): |
2846 | + """Echo filtered attributes back onto the same relation for storage. |
2847 | + |
2848 | + This is a requirement to use the peerstorage module - it needs to be called |
2849 | + from the peer relation's changed hook. |
2850 | + |
2851 | + If Juju leader support exists this will be a noop unless force is True. |
2852 | + """ |
2853 | + try: |
2854 | + is_leader() |
2855 | + except NotImplementedError: |
2856 | + pass |
2857 | + else: |
2858 | + if not force: |
2859 | + return # NOOP if leader-election is supported |
2860 | + |
2861 | + # Use original non-leader calls |
2862 | + relation_get = _relation_get |
2863 | + relation_set = _relation_set |
2864 | + |
2865 | + rdata = relation_get() |
2866 | + echo_data = {} |
2867 | + if includes is None: |
2868 | + echo_data = rdata.copy() |
2869 | + for ex in ['private-address', 'public-address']: |
2870 | + if ex in echo_data: |
2871 | + echo_data.pop(ex) |
2872 | + else: |
2873 | + for attribute, value in six.iteritems(rdata): |
2874 | + for include in includes: |
2875 | + if include in attribute: |
2876 | + echo_data[attribute] = value |
2877 | + if len(echo_data) > 0: |
2878 | + relation_set(relation_settings=echo_data) |
2879 | + |
2880 | + |
2881 | +def peer_store_and_set(relation_id=None, peer_relation_name='cluster', |
2882 | + peer_store_fatal=False, relation_settings=None, |
2883 | + delimiter='_', **kwargs): |
2884 | + """Store passed-in arguments both in argument relation and in peer storage. |
2885 | + |
2886 | + It functions like doing relation_set() and peer_store() at the same time, |
2887 | + with the same data. |
2888 | + |
2889 | + @param relation_id: the id of the relation to store the data on. Defaults |
2890 | + to the current relation. |
2891 | + @param peer_store_fatal: Set to True, the function will raise an exception |
2892 | + should the peer sotrage not be avialable.""" |
2893 | + |
2894 | + relation_settings = relation_settings if relation_settings else {} |
2895 | + relation_set(relation_id=relation_id, |
2896 | + relation_settings=relation_settings, |
2897 | + **kwargs) |
2898 | + if is_relation_made(peer_relation_name): |
2899 | + for key, value in six.iteritems(dict(list(kwargs.items()) + |
2900 | + list(relation_settings.items()))): |
2901 | + key_prefix = relation_id or current_relation_id() |
2902 | + peer_store(key_prefix + delimiter + key, |
2903 | + value, |
2904 | + relation_name=peer_relation_name) |
2905 | + else: |
2906 | + if peer_store_fatal: |
2907 | + raise ValueError('Unable to detect ' |
2908 | + 'peer relation {}'.format(peer_relation_name)) |
2909 | |
2910 | === added directory 'charmhelpers/core' |
2911 | === added file 'charmhelpers/core/__init__.py' |
2912 | --- charmhelpers/core/__init__.py 1970-01-01 00:00:00 +0000 |
2913 | +++ charmhelpers/core/__init__.py 2016-03-10 22:55:31 +0000 |
2914 | @@ -0,0 +1,15 @@ |
2915 | +# Copyright 2014-2015 Canonical Limited. |
2916 | +# |
2917 | +# This file is part of charm-helpers. |
2918 | +# |
2919 | +# charm-helpers is free software: you can redistribute it and/or modify |
2920 | +# it under the terms of the GNU Lesser General Public License version 3 as |
2921 | +# published by the Free Software Foundation. |
2922 | +# |
2923 | +# charm-helpers is distributed in the hope that it will be useful, |
2924 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
2925 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
2926 | +# GNU Lesser General Public License for more details. |
2927 | +# |
2928 | +# You should have received a copy of the GNU Lesser General Public License |
2929 | +# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
2930 | |
2931 | === added file 'charmhelpers/core/decorators.py' |
2932 | --- charmhelpers/core/decorators.py 1970-01-01 00:00:00 +0000 |
2933 | +++ charmhelpers/core/decorators.py 2016-03-10 22:55:31 +0000 |
2934 | @@ -0,0 +1,57 @@ |
2935 | +# Copyright 2014-2015 Canonical Limited. |
2936 | +# |
2937 | +# This file is part of charm-helpers. |
2938 | +# |
2939 | +# charm-helpers is free software: you can redistribute it and/or modify |
2940 | +# it under the terms of the GNU Lesser General Public License version 3 as |
2941 | +# published by the Free Software Foundation. |
2942 | +# |
2943 | +# charm-helpers is distributed in the hope that it will be useful, |
2944 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
2945 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
2946 | +# GNU Lesser General Public License for more details. |
2947 | +# |
2948 | +# You should have received a copy of the GNU Lesser General Public License |
2949 | +# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
2950 | + |
2951 | +# |
2952 | +# Copyright 2014 Canonical Ltd. |
2953 | +# |
2954 | +# Authors: |
2955 | +# Edward Hope-Morley <opentastic@gmail.com> |
2956 | +# |
2957 | + |
2958 | +import time |
2959 | + |
2960 | +from charmhelpers.core.hookenv import ( |
2961 | + log, |
2962 | + INFO, |
2963 | +) |
2964 | + |
2965 | + |
2966 | +def retry_on_exception(num_retries, base_delay=0, exc_type=Exception): |
2967 | + """If the decorated function raises exception exc_type, allow num_retries |
2968 | + retry attempts before raise the exception. |
2969 | + """ |
2970 | + def _retry_on_exception_inner_1(f): |
2971 | + def _retry_on_exception_inner_2(*args, **kwargs): |
2972 | + retries = num_retries |
2973 | + multiplier = 1 |
2974 | + while True: |
2975 | + try: |
2976 | + return f(*args, **kwargs) |
2977 | + except exc_type: |
2978 | + if not retries: |
2979 | + raise |
2980 | + |
2981 | + delay = base_delay * multiplier |
2982 | + multiplier += 1 |
2983 | + log("Retrying '%s' %d more times (delay=%s)" % |
2984 | + (f.__name__, retries, delay), level=INFO) |
2985 | + retries -= 1 |
2986 | + if delay: |
2987 | + time.sleep(delay) |
2988 | + |
2989 | + return _retry_on_exception_inner_2 |
2990 | + |
2991 | + return _retry_on_exception_inner_1 |
2992 | |
2993 | === added file 'charmhelpers/core/files.py' |
2994 | --- charmhelpers/core/files.py 1970-01-01 00:00:00 +0000 |
2995 | +++ charmhelpers/core/files.py 2016-03-10 22:55:31 +0000 |
2996 | @@ -0,0 +1,45 @@ |
2997 | +#!/usr/bin/env python |
2998 | +# -*- coding: utf-8 -*- |
2999 | + |
3000 | +# Copyright 2014-2015 Canonical Limited. |
3001 | +# |
3002 | +# This file is part of charm-helpers. |
3003 | +# |
3004 | +# charm-helpers is free software: you can redistribute it and/or modify |
3005 | +# it under the terms of the GNU Lesser General Public License version 3 as |
3006 | +# published by the Free Software Foundation. |
3007 | +# |
3008 | +# charm-helpers is distributed in the hope that it will be useful, |
3009 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
3010 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
3011 | +# GNU Lesser General Public License for more details. |
3012 | +# |
3013 | +# You should have received a copy of the GNU Lesser General Public License |
3014 | +# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
3015 | + |
3016 | +__author__ = 'Jorge Niedbalski <niedbalski@ubuntu.com>' |
3017 | + |
3018 | +import os |
3019 | +import subprocess |
3020 | + |
3021 | + |
3022 | +def sed(filename, before, after, flags='g'): |
3023 | + """ |
3024 | + Search and replaces the given pattern on filename. |
3025 | + |
3026 | + :param filename: relative or absolute file path. |
3027 | + :param before: expression to be replaced (see 'man sed') |
3028 | + :param after: expression to replace with (see 'man sed') |
3029 | + :param flags: sed-compatible regex flags in example, to make |
3030 | + the search and replace case insensitive, specify ``flags="i"``. |
3031 | + The ``g`` flag is always specified regardless, so you do not |
3032 | + need to remember to include it when overriding this parameter. |
3033 | + :returns: If the sed command exit code was zero then return, |
3034 | + otherwise raise CalledProcessError. |
3035 | + """ |
3036 | + expression = r's/{0}/{1}/{2}'.format(before, |
3037 | + after, flags) |
3038 | + |
3039 | + return subprocess.check_call(["sed", "-i", "-r", "-e", |
3040 | + expression, |
3041 | + os.path.expanduser(filename)]) |
3042 | |
3043 | === added file 'charmhelpers/core/fstab.py' |
3044 | --- charmhelpers/core/fstab.py 1970-01-01 00:00:00 +0000 |
3045 | +++ charmhelpers/core/fstab.py 2016-03-10 22:55:31 +0000 |
3046 | @@ -0,0 +1,134 @@ |
3047 | +#!/usr/bin/env python |
3048 | +# -*- coding: utf-8 -*- |
3049 | + |
3050 | +# Copyright 2014-2015 Canonical Limited. |
3051 | +# |
3052 | +# This file is part of charm-helpers. |
3053 | +# |
3054 | +# charm-helpers is free software: you can redistribute it and/or modify |
3055 | +# it under the terms of the GNU Lesser General Public License version 3 as |
3056 | +# published by the Free Software Foundation. |
3057 | +# |
3058 | +# charm-helpers is distributed in the hope that it will be useful, |
3059 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
3060 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
3061 | +# GNU Lesser General Public License for more details. |
3062 | +# |
3063 | +# You should have received a copy of the GNU Lesser General Public License |
3064 | +# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
3065 | + |
3066 | +import io |
3067 | +import os |
3068 | + |
3069 | +__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>' |
3070 | + |
3071 | + |
3072 | +class Fstab(io.FileIO): |
3073 | + """This class extends file in order to implement a file reader/writer |
3074 | + for file `/etc/fstab` |
3075 | + """ |
3076 | + |
3077 | + class Entry(object): |
3078 | + """Entry class represents a non-comment line on the `/etc/fstab` file |
3079 | + """ |
3080 | + def __init__(self, device, mountpoint, filesystem, |
3081 | + options, d=0, p=0): |
3082 | + self.device = device |
3083 | + self.mountpoint = mountpoint |
3084 | + self.filesystem = filesystem |
3085 | + |
3086 | + if not options: |
3087 | + options = "defaults" |
3088 | + |
3089 | + self.options = options |
3090 | + self.d = int(d) |
3091 | + self.p = int(p) |
3092 | + |
3093 | + def __eq__(self, o): |
3094 | + return str(self) == str(o) |
3095 | + |
3096 | + def __str__(self): |
3097 | + return "{} {} {} {} {} {}".format(self.device, |
3098 | + self.mountpoint, |
3099 | + self.filesystem, |
3100 | + self.options, |
3101 | + self.d, |
3102 | + self.p) |
3103 | + |
3104 | + DEFAULT_PATH = os.path.join(os.path.sep, 'etc', 'fstab') |
3105 | + |
3106 | + def __init__(self, path=None): |
3107 | + if path: |
3108 | + self._path = path |
3109 | + else: |
3110 | + self._path = self.DEFAULT_PATH |
3111 | + super(Fstab, self).__init__(self._path, 'rb+') |
3112 | + |
3113 | + def _hydrate_entry(self, line): |
3114 | + # NOTE: use split with no arguments to split on any |
3115 | + # whitespace including tabs |
3116 | + return Fstab.Entry(*filter( |
3117 | + lambda x: x not in ('', None), |
3118 | + line.strip("\n").split())) |
3119 | + |
3120 | + @property |
3121 | + def entries(self): |
3122 | + self.seek(0) |
3123 | + for line in self.readlines(): |
3124 | + line = line.decode('us-ascii') |
3125 | + try: |
3126 | + if line.strip() and not line.strip().startswith("#"): |
3127 | + yield self._hydrate_entry(line) |
3128 | + except ValueError: |
3129 | + pass |
3130 | + |
3131 | + def get_entry_by_attr(self, attr, value): |
3132 | + for entry in self.entries: |
3133 | + e_attr = getattr(entry, attr) |
3134 | + if e_attr == value: |
3135 | + return entry |
3136 | + return None |
3137 | + |
3138 | + def add_entry(self, entry): |
3139 | + if self.get_entry_by_attr('device', entry.device): |
3140 | + return False |
3141 | + |
3142 | + self.write((str(entry) + '\n').encode('us-ascii')) |
3143 | + self.truncate() |
3144 | + return entry |
3145 | + |
3146 | + def remove_entry(self, entry): |
3147 | + self.seek(0) |
3148 | + |
3149 | + lines = [l.decode('us-ascii') for l in self.readlines()] |
3150 | + |
3151 | + found = False |
3152 | + for index, line in enumerate(lines): |
3153 | + if line.strip() and not line.strip().startswith("#"): |
3154 | + if self._hydrate_entry(line) == entry: |
3155 | + found = True |
3156 | + break |
3157 | + |
3158 | + if not found: |
3159 | + return False |
3160 | + |
3161 | + lines.remove(line) |
3162 | + |
3163 | + self.seek(0) |
3164 | + self.write(''.join(lines).encode('us-ascii')) |
3165 | + self.truncate() |
3166 | + return True |
3167 | + |
3168 | + @classmethod |
3169 | + def remove_by_mountpoint(cls, mountpoint, path=None): |
3170 | + fstab = cls(path=path) |
3171 | + entry = fstab.get_entry_by_attr('mountpoint', mountpoint) |
3172 | + if entry: |
3173 | + return fstab.remove_entry(entry) |
3174 | + return False |
3175 | + |
3176 | + @classmethod |
3177 | + def add(cls, device, mountpoint, filesystem, options=None, path=None): |
3178 | + return cls(path=path).add_entry(Fstab.Entry(device, |
3179 | + mountpoint, filesystem, |
3180 | + options=options)) |
3181 | |
3182 | === added file 'charmhelpers/core/hookenv.py' |
3183 | --- charmhelpers/core/hookenv.py 1970-01-01 00:00:00 +0000 |
3184 | +++ charmhelpers/core/hookenv.py 2016-03-10 22:55:31 +0000 |
3185 | @@ -0,0 +1,978 @@ |
3186 | +# Copyright 2014-2015 Canonical Limited. |
3187 | +# |
3188 | +# This file is part of charm-helpers. |
3189 | +# |
3190 | +# charm-helpers is free software: you can redistribute it and/or modify |
3191 | +# it under the terms of the GNU Lesser General Public License version 3 as |
3192 | +# published by the Free Software Foundation. |
3193 | +# |
3194 | +# charm-helpers is distributed in the hope that it will be useful, |
3195 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
3196 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
3197 | +# GNU Lesser General Public License for more details. |
3198 | +# |
3199 | +# You should have received a copy of the GNU Lesser General Public License |
3200 | +# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
3201 | + |
3202 | +"Interactions with the Juju environment" |
3203 | +# Copyright 2013 Canonical Ltd. |
3204 | +# |
3205 | +# Authors: |
3206 | +# Charm Helpers Developers <juju@lists.ubuntu.com> |
3207 | + |
3208 | +from __future__ import print_function |
3209 | +import copy |
3210 | +from distutils.version import LooseVersion |
3211 | +from functools import wraps |
3212 | +import glob |
3213 | +import os |
3214 | +import json |
3215 | +import yaml |
3216 | +import subprocess |
3217 | +import sys |
3218 | +import errno |
3219 | +import tempfile |
3220 | +from subprocess import CalledProcessError |
3221 | + |
3222 | +import six |
3223 | +if not six.PY3: |
3224 | + from UserDict import UserDict |
3225 | +else: |
3226 | + from collections import UserDict |
3227 | + |
3228 | +CRITICAL = "CRITICAL" |
3229 | +ERROR = "ERROR" |
3230 | +WARNING = "WARNING" |
3231 | +INFO = "INFO" |
3232 | +DEBUG = "DEBUG" |
3233 | +MARKER = object() |
3234 | + |
3235 | +cache = {} |
3236 | + |
3237 | + |
3238 | +def cached(func): |
3239 | + """Cache return values for multiple executions of func + args |
3240 | + |
3241 | + For example:: |
3242 | + |
3243 | + @cached |
3244 | + def unit_get(attribute): |
3245 | + pass |
3246 | + |
3247 | + unit_get('test') |
3248 | + |
3249 | + will cache the result of unit_get + 'test' for future calls. |
3250 | + """ |
3251 | + @wraps(func) |
3252 | + def wrapper(*args, **kwargs): |
3253 | + global cache |
3254 | + key = str((func, args, kwargs)) |
3255 | + try: |
3256 | + return cache[key] |
3257 | + except KeyError: |
3258 | + pass # Drop out of the exception handler scope. |
3259 | + res = func(*args, **kwargs) |
3260 | + cache[key] = res |
3261 | + return res |
3262 | + wrapper._wrapped = func |
3263 | + return wrapper |
3264 | + |
3265 | + |
3266 | +def flush(key): |
3267 | + """Flushes any entries from function cache where the |
3268 | + key is found in the function+args """ |
3269 | + flush_list = [] |
3270 | + for item in cache: |
3271 | + if key in item: |
3272 | + flush_list.append(item) |
3273 | + for item in flush_list: |
3274 | + del cache[item] |
3275 | + |
3276 | + |
3277 | +def log(message, level=None): |
3278 | + """Write a message to the juju log""" |
3279 | + command = ['juju-log'] |
3280 | + if level: |
3281 | + command += ['-l', level] |
3282 | + if not isinstance(message, six.string_types): |
3283 | + message = repr(message) |
3284 | + command += [message] |
3285 | + # Missing juju-log should not cause failures in unit tests |
3286 | + # Send log output to stderr |
3287 | + try: |
3288 | + subprocess.call(command) |
3289 | + except OSError as e: |
3290 | + if e.errno == errno.ENOENT: |
3291 | + if level: |
3292 | + message = "{}: {}".format(level, message) |
3293 | + message = "juju-log: {}".format(message) |
3294 | + print(message, file=sys.stderr) |
3295 | + else: |
3296 | + raise |
3297 | + |
3298 | + |
3299 | +class Serializable(UserDict): |
3300 | + """Wrapper, an object that can be serialized to yaml or json""" |
3301 | + |
3302 | + def __init__(self, obj): |
3303 | + # wrap the object |
3304 | + UserDict.__init__(self) |
3305 | + self.data = obj |
3306 | + |
3307 | + def __getattr__(self, attr): |
3308 | + # See if this object has attribute. |
3309 | + if attr in ("json", "yaml", "data"): |
3310 | + return self.__dict__[attr] |
3311 | + # Check for attribute in wrapped object. |
3312 | + got = getattr(self.data, attr, MARKER) |
3313 | + if got is not MARKER: |
3314 | + return got |
3315 | + # Proxy to the wrapped object via dict interface. |
3316 | + try: |
3317 | + return self.data[attr] |
3318 | + except KeyError: |
3319 | + raise AttributeError(attr) |
3320 | + |
3321 | + def __getstate__(self): |
3322 | + # Pickle as a standard dictionary. |
3323 | + return self.data |
3324 | + |
3325 | + def __setstate__(self, state): |
3326 | + # Unpickle into our wrapper. |
3327 | + self.data = state |
3328 | + |
3329 | + def json(self): |
3330 | + """Serialize the object to json""" |
3331 | + return json.dumps(self.data) |
3332 | + |
3333 | + def yaml(self): |
3334 | + """Serialize the object to yaml""" |
3335 | + return yaml.dump(self.data) |
3336 | + |
3337 | + |
3338 | +def execution_environment(): |
3339 | + """A convenient bundling of the current execution context""" |
3340 | + context = {} |
3341 | + context['conf'] = config() |
3342 | + if relation_id(): |
3343 | + context['reltype'] = relation_type() |
3344 | + context['relid'] = relation_id() |
3345 | + context['rel'] = relation_get() |
3346 | + context['unit'] = local_unit() |
3347 | + context['rels'] = relations() |
3348 | + context['env'] = os.environ |
3349 | + return context |
3350 | + |
3351 | + |
3352 | +def in_relation_hook(): |
3353 | + """Determine whether we're running in a relation hook""" |
3354 | + return 'JUJU_RELATION' in os.environ |
3355 | + |
3356 | + |
3357 | +def relation_type(): |
3358 | + """The scope for the current relation hook""" |
3359 | + return os.environ.get('JUJU_RELATION', None) |
3360 | + |
3361 | + |
3362 | +@cached |
3363 | +def relation_id(relation_name=None, service_or_unit=None): |
3364 | + """The relation ID for the current or a specified relation""" |
3365 | + if not relation_name and not service_or_unit: |
3366 | + return os.environ.get('JUJU_RELATION_ID', None) |
3367 | + elif relation_name and service_or_unit: |
3368 | + service_name = service_or_unit.split('/')[0] |
3369 | + for relid in relation_ids(relation_name): |
3370 | + remote_service = remote_service_name(relid) |
3371 | + if remote_service == service_name: |
3372 | + return relid |
3373 | + else: |
3374 | + raise ValueError('Must specify neither or both of relation_name and service_or_unit') |
3375 | + |
3376 | + |
3377 | +def local_unit(): |
3378 | + """Local unit ID""" |
3379 | + return os.environ['JUJU_UNIT_NAME'] |
3380 | + |
3381 | + |
3382 | +def remote_unit(): |
3383 | + """The remote unit for the current relation hook""" |
3384 | + return os.environ.get('JUJU_REMOTE_UNIT', None) |
3385 | + |
3386 | + |
3387 | +def service_name(): |
3388 | + """The name service group this unit belongs to""" |
3389 | + return local_unit().split('/')[0] |
3390 | + |
3391 | + |
3392 | +@cached |
3393 | +def remote_service_name(relid=None): |
3394 | + """The remote service name for a given relation-id (or the current relation)""" |
3395 | + if relid is None: |
3396 | + unit = remote_unit() |
3397 | + else: |
3398 | + units = related_units(relid) |
3399 | + unit = units[0] if units else None |
3400 | + return unit.split('/')[0] if unit else None |
3401 | + |
3402 | + |
3403 | +def hook_name(): |
3404 | + """The name of the currently executing hook""" |
3405 | + return os.environ.get('JUJU_HOOK_NAME', os.path.basename(sys.argv[0])) |
3406 | + |
3407 | + |
3408 | +class Config(dict): |
3409 | + """A dictionary representation of the charm's config.yaml, with some |
3410 | + extra features: |
3411 | + |
3412 | + - See which values in the dictionary have changed since the previous hook. |
3413 | + - For values that have changed, see what the previous value was. |
3414 | + - Store arbitrary data for use in a later hook. |
3415 | + |
3416 | + NOTE: Do not instantiate this object directly - instead call |
3417 | + ``hookenv.config()``, which will return an instance of :class:`Config`. |
3418 | + |
3419 | + Example usage:: |
3420 | + |
3421 | + >>> # inside a hook |
3422 | + >>> from charmhelpers.core import hookenv |
3423 | + >>> config = hookenv.config() |
3424 | + >>> config['foo'] |
3425 | + 'bar' |
3426 | + >>> # store a new key/value for later use |
3427 | + >>> config['mykey'] = 'myval' |
3428 | + |
3429 | + |
3430 | + >>> # user runs `juju set mycharm foo=baz` |
3431 | + >>> # now we're inside subsequent config-changed hook |
3432 | + >>> config = hookenv.config() |
3433 | + >>> config['foo'] |
3434 | + 'baz' |
3435 | + >>> # test to see if this val has changed since last hook |
3436 | + >>> config.changed('foo') |
3437 | + True |
3438 | + >>> # what was the previous value? |
3439 | + >>> config.previous('foo') |
3440 | + 'bar' |
3441 | + >>> # keys/values that we add are preserved across hooks |
3442 | + >>> config['mykey'] |
3443 | + 'myval' |
3444 | + |
3445 | + """ |
3446 | + CONFIG_FILE_NAME = '.juju-persistent-config' |
3447 | + |
3448 | + def __init__(self, *args, **kw): |
3449 | + super(Config, self).__init__(*args, **kw) |
3450 | + self.implicit_save = True |
3451 | + self._prev_dict = None |
3452 | + self.path = os.path.join(charm_dir(), Config.CONFIG_FILE_NAME) |
3453 | + if os.path.exists(self.path): |
3454 | + self.load_previous() |
3455 | + atexit(self._implicit_save) |
3456 | + |
3457 | + def load_previous(self, path=None): |
3458 | + """Load previous copy of config from disk. |
3459 | + |
3460 | + In normal usage you don't need to call this method directly - it |
3461 | + is called automatically at object initialization. |
3462 | + |
3463 | + :param path: |
3464 | + |
3465 | + File path from which to load the previous config. If `None`, |
3466 | + config is loaded from the default location. If `path` is |
3467 | + specified, subsequent `save()` calls will write to the same |
3468 | + path. |
3469 | + |
3470 | + """ |
3471 | + self.path = path or self.path |
3472 | + with open(self.path) as f: |
3473 | + self._prev_dict = json.load(f) |
3474 | + for k, v in copy.deepcopy(self._prev_dict).items(): |
3475 | + if k not in self: |
3476 | + self[k] = v |
3477 | + |
3478 | + def changed(self, key): |
3479 | + """Return True if the current value for this key is different from |
3480 | + the previous value. |
3481 | + |
3482 | + """ |
3483 | + if self._prev_dict is None: |
3484 | + return True |
3485 | + return self.previous(key) != self.get(key) |
3486 | + |
3487 | + def previous(self, key): |
3488 | + """Return previous value for this key, or None if there |
3489 | + is no previous value. |
3490 | + |
3491 | + """ |
3492 | + if self._prev_dict: |
3493 | + return self._prev_dict.get(key) |
3494 | + return None |
3495 | + |
3496 | + def save(self): |
3497 | + """Save this config to disk. |
3498 | + |
3499 | + If the charm is using the :mod:`Services Framework <services.base>` |
3500 | + or :meth:'@hook <Hooks.hook>' decorator, this |
3501 | + is called automatically at the end of successful hook execution. |
3502 | + Otherwise, it should be called directly by user code. |
3503 | + |
3504 | + To disable automatic saves, set ``implicit_save=False`` on this |
3505 | + instance. |
3506 | + |
3507 | + """ |
3508 | + with open(self.path, 'w') as f: |
3509 | + json.dump(self, f) |
3510 | + |
3511 | + def _implicit_save(self): |
3512 | + if self.implicit_save: |
3513 | + self.save() |
3514 | + |
3515 | + |
3516 | +@cached |
3517 | +def config(scope=None): |
3518 | + """Juju charm configuration""" |
3519 | + config_cmd_line = ['config-get'] |
3520 | + if scope is not None: |
3521 | + config_cmd_line.append(scope) |
3522 | + config_cmd_line.append('--format=json') |
3523 | + try: |
3524 | + config_data = json.loads( |
3525 | + subprocess.check_output(config_cmd_line).decode('UTF-8')) |
3526 | + if scope is not None: |
3527 | + return config_data |
3528 | + return Config(config_data) |
3529 | + except ValueError: |
3530 | + return None |
3531 | + |
3532 | + |
3533 | +@cached |
3534 | +def relation_get(attribute=None, unit=None, rid=None): |
3535 | + """Get relation information""" |
3536 | + _args = ['relation-get', '--format=json'] |
3537 | + if rid: |
3538 | + _args.append('-r') |
3539 | + _args.append(rid) |
3540 | + _args.append(attribute or '-') |
3541 | + if unit: |
3542 | + _args.append(unit) |
3543 | + try: |
3544 | + return json.loads(subprocess.check_output(_args).decode('UTF-8')) |
3545 | + except ValueError: |
3546 | + return None |
3547 | + except CalledProcessError as e: |
3548 | + if e.returncode == 2: |
3549 | + return None |
3550 | + raise |
3551 | + |
3552 | + |
3553 | +def relation_set(relation_id=None, relation_settings=None, **kwargs): |
3554 | + """Set relation information for the current unit""" |
3555 | + relation_settings = relation_settings if relation_settings else {} |
3556 | + relation_cmd_line = ['relation-set'] |
3557 | + accepts_file = "--file" in subprocess.check_output( |
3558 | + relation_cmd_line + ["--help"], universal_newlines=True) |
3559 | + if relation_id is not None: |
3560 | + relation_cmd_line.extend(('-r', relation_id)) |
3561 | + settings = relation_settings.copy() |
3562 | + settings.update(kwargs) |
3563 | + for key, value in settings.items(): |
3564 | + # Force value to be a string: it always should, but some call |
3565 | + # sites pass in things like dicts or numbers. |
3566 | + if value is not None: |
3567 | + settings[key] = "{}".format(value) |
3568 | + if accepts_file: |
3569 | + # --file was introduced in Juju 1.23.2. Use it by default if |
3570 | + # available, since otherwise we'll break if the relation data is |
3571 | + # too big. Ideally we should tell relation-set to read the data from |
3572 | + # stdin, but that feature is broken in 1.23.2: Bug #1454678. |
3573 | + with tempfile.NamedTemporaryFile(delete=False) as settings_file: |
3574 | + settings_file.write(yaml.safe_dump(settings).encode("utf-8")) |
3575 | + subprocess.check_call( |
3576 | + relation_cmd_line + ["--file", settings_file.name]) |
3577 | + os.remove(settings_file.name) |
3578 | + else: |
3579 | + for key, value in settings.items(): |
3580 | + if value is None: |
3581 | + relation_cmd_line.append('{}='.format(key)) |
3582 | + else: |
3583 | + relation_cmd_line.append('{}={}'.format(key, value)) |
3584 | + subprocess.check_call(relation_cmd_line) |
3585 | + # Flush cache of any relation-gets for local unit |
3586 | + flush(local_unit()) |
3587 | + |
3588 | + |
3589 | +def relation_clear(r_id=None): |
3590 | + ''' Clears any relation data already set on relation r_id ''' |
3591 | + settings = relation_get(rid=r_id, |
3592 | + unit=local_unit()) |
3593 | + for setting in settings: |
3594 | + if setting not in ['public-address', 'private-address']: |
3595 | + settings[setting] = None |
3596 | + relation_set(relation_id=r_id, |
3597 | + **settings) |
3598 | + |
3599 | + |
3600 | +@cached |
3601 | +def relation_ids(reltype=None): |
3602 | + """A list of relation_ids""" |
3603 | + reltype = reltype or relation_type() |
3604 | + relid_cmd_line = ['relation-ids', '--format=json'] |
3605 | + if reltype is not None: |
3606 | + relid_cmd_line.append(reltype) |
3607 | + return json.loads( |
3608 | + subprocess.check_output(relid_cmd_line).decode('UTF-8')) or [] |
3609 | + return [] |
3610 | + |
3611 | + |
3612 | +@cached |
3613 | +def related_units(relid=None): |
3614 | + """A list of related units""" |
3615 | + relid = relid or relation_id() |
3616 | + units_cmd_line = ['relation-list', '--format=json'] |
3617 | + if relid is not None: |
3618 | + units_cmd_line.extend(('-r', relid)) |
3619 | + return json.loads( |
3620 | + subprocess.check_output(units_cmd_line).decode('UTF-8')) or [] |
3621 | + |
3622 | + |
3623 | +@cached |
3624 | +def relation_for_unit(unit=None, rid=None): |
3625 | + """Get the json represenation of a unit's relation""" |
3626 | + unit = unit or remote_unit() |
3627 | + relation = relation_get(unit=unit, rid=rid) |
3628 | + for key in relation: |
3629 | + if key.endswith('-list'): |
3630 | + relation[key] = relation[key].split() |
3631 | + relation['__unit__'] = unit |
3632 | + return relation |
3633 | + |
3634 | + |
3635 | +@cached |
3636 | +def relations_for_id(relid=None): |
3637 | + """Get relations of a specific relation ID""" |
3638 | + relation_data = [] |
3639 | + relid = relid or relation_ids() |
3640 | + for unit in related_units(relid): |
3641 | + unit_data = relation_for_unit(unit, relid) |
3642 | + unit_data['__relid__'] = relid |
3643 | + relation_data.append(unit_data) |
3644 | + return relation_data |
3645 | + |
3646 | + |
3647 | +@cached |
3648 | +def relations_of_type(reltype=None): |
3649 | + """Get relations of a specific type""" |
3650 | + relation_data = [] |
3651 | + reltype = reltype or relation_type() |
3652 | + for relid in relation_ids(reltype): |
3653 | + for relation in relations_for_id(relid): |
3654 | + relation['__relid__'] = relid |
3655 | + relation_data.append(relation) |
3656 | + return relation_data |
3657 | + |
3658 | + |
3659 | +@cached |
3660 | +def metadata(): |
3661 | + """Get the current charm metadata.yaml contents as a python object""" |
3662 | + with open(os.path.join(charm_dir(), 'metadata.yaml')) as md: |
3663 | + return yaml.safe_load(md) |
3664 | + |
3665 | + |
3666 | +@cached |
3667 | +def relation_types(): |
3668 | + """Get a list of relation types supported by this charm""" |
3669 | + rel_types = [] |
3670 | + md = metadata() |
3671 | + for key in ('provides', 'requires', 'peers'): |
3672 | + section = md.get(key) |
3673 | + if section: |
3674 | + rel_types.extend(section.keys()) |
3675 | + return rel_types |
3676 | + |
3677 | + |
3678 | +@cached |
3679 | +def peer_relation_id(): |
3680 | + '''Get the peers relation id if a peers relation has been joined, else None.''' |
3681 | + md = metadata() |
3682 | + section = md.get('peers') |
3683 | + if section: |
3684 | + for key in section: |
3685 | + relids = relation_ids(key) |
3686 | + if relids: |
3687 | + return relids[0] |
3688 | + return None |
3689 | + |
3690 | + |
3691 | +@cached |
3692 | +def relation_to_interface(relation_name): |
3693 | + """ |
3694 | + Given the name of a relation, return the interface that relation uses. |
3695 | + |
3696 | + :returns: The interface name, or ``None``. |
3697 | + """ |
3698 | + return relation_to_role_and_interface(relation_name)[1] |
3699 | + |
3700 | + |
3701 | +@cached |
3702 | +def relation_to_role_and_interface(relation_name): |
3703 | + """ |
3704 | + Given the name of a relation, return the role and the name of the interface |
3705 | + that relation uses (where role is one of ``provides``, ``requires``, or ``peers``). |
3706 | + |
3707 | + :returns: A tuple containing ``(role, interface)``, or ``(None, None)``. |
3708 | + """ |
3709 | + _metadata = metadata() |
3710 | + for role in ('provides', 'requires', 'peers'): |
3711 | + interface = _metadata.get(role, {}).get(relation_name, {}).get('interface') |
3712 | + if interface: |
3713 | + return role, interface |
3714 | + return None, None |
3715 | + |
3716 | + |
3717 | +@cached |
3718 | +def role_and_interface_to_relations(role, interface_name): |
3719 | + """ |
3720 | + Given a role and interface name, return a list of relation names for the |
3721 | + current charm that use that interface under that role (where role is one |
3722 | + of ``provides``, ``requires``, or ``peers``). |
3723 | + |
3724 | + :returns: A list of relation names. |
3725 | + """ |
3726 | + _metadata = metadata() |
3727 | + results = [] |
3728 | + for relation_name, relation in _metadata.get(role, {}).items(): |
3729 | + if relation['interface'] == interface_name: |
3730 | + results.append(relation_name) |
3731 | + return results |
3732 | + |
3733 | + |
3734 | +@cached |
3735 | +def interface_to_relations(interface_name): |
3736 | + """ |
3737 | + Given an interface, return a list of relation names for the current |
3738 | + charm that use that interface. |
3739 | + |
3740 | + :returns: A list of relation names. |
3741 | + """ |
3742 | + results = [] |
3743 | + for role in ('provides', 'requires', 'peers'): |
3744 | + results.extend(role_and_interface_to_relations(role, interface_name)) |
3745 | + return results |
3746 | + |
3747 | + |
3748 | +@cached |
3749 | +def charm_name(): |
3750 | + """Get the name of the current charm as is specified on metadata.yaml""" |
3751 | + return metadata().get('name') |
3752 | + |
3753 | + |
3754 | +@cached |
3755 | +def relations(): |
3756 | + """Get a nested dictionary of relation data for all related units""" |
3757 | + rels = {} |
3758 | + for reltype in relation_types(): |
3759 | + relids = {} |
3760 | + for relid in relation_ids(reltype): |
3761 | + units = {local_unit(): relation_get(unit=local_unit(), rid=relid)} |
3762 | + for unit in related_units(relid): |
3763 | + reldata = relation_get(unit=unit, rid=relid) |
3764 | + units[unit] = reldata |
3765 | + relids[relid] = units |
3766 | + rels[reltype] = relids |
3767 | + return rels |
3768 | + |
3769 | + |
3770 | +@cached |
3771 | +def is_relation_made(relation, keys='private-address'): |
3772 | + ''' |
3773 | + Determine whether a relation is established by checking for |
3774 | + presence of key(s). If a list of keys is provided, they |
3775 | + must all be present for the relation to be identified as made |
3776 | + ''' |
3777 | + if isinstance(keys, str): |
3778 | + keys = [keys] |
3779 | + for r_id in relation_ids(relation): |
3780 | + for unit in related_units(r_id): |
3781 | + context = {} |
3782 | + for k in keys: |
3783 | + context[k] = relation_get(k, rid=r_id, |
3784 | + unit=unit) |
3785 | + if None not in context.values(): |
3786 | + return True |
3787 | + return False |
3788 | + |
3789 | + |
3790 | +def open_port(port, protocol="TCP"): |
3791 | + """Open a service network port""" |
3792 | + _args = ['open-port'] |
3793 | + _args.append('{}/{}'.format(port, protocol)) |
3794 | + subprocess.check_call(_args) |
3795 | + |
3796 | + |
3797 | +def close_port(port, protocol="TCP"): |
3798 | + """Close a service network port""" |
3799 | + _args = ['close-port'] |
3800 | + _args.append('{}/{}'.format(port, protocol)) |
3801 | + subprocess.check_call(_args) |
3802 | + |
3803 | + |
3804 | +@cached |
3805 | +def unit_get(attribute): |
3806 | + """Get the unit ID for the remote unit""" |
3807 | + _args = ['unit-get', '--format=json', attribute] |
3808 | + try: |
3809 | + return json.loads(subprocess.check_output(_args).decode('UTF-8')) |
3810 | + except ValueError: |
3811 | + return None |
3812 | + |
3813 | + |
3814 | +def unit_public_ip(): |
3815 | + """Get this unit's public IP address""" |
3816 | + return unit_get('public-address') |
3817 | + |
3818 | + |
3819 | +def unit_private_ip(): |
3820 | + """Get this unit's private IP address""" |
3821 | + return unit_get('private-address') |
3822 | + |
3823 | + |
3824 | +@cached |
3825 | +def storage_get(attribute=None, storage_id=None): |
3826 | + """Get storage attributes""" |
3827 | + _args = ['storage-get', '--format=json'] |
3828 | + if storage_id: |
3829 | + _args.extend(('-s', storage_id)) |
3830 | + if attribute: |
3831 | + _args.append(attribute) |
3832 | + try: |
3833 | + return json.loads(subprocess.check_output(_args).decode('UTF-8')) |
3834 | + except ValueError: |
3835 | + return None |
3836 | + |
3837 | + |
3838 | +@cached |
3839 | +def storage_list(storage_name=None): |
3840 | + """List the storage IDs for the unit""" |
3841 | + _args = ['storage-list', '--format=json'] |
3842 | + if storage_name: |
3843 | + _args.append(storage_name) |
3844 | + try: |
3845 | + return json.loads(subprocess.check_output(_args).decode('UTF-8')) |
3846 | + except ValueError: |
3847 | + return None |
3848 | + except OSError as e: |
3849 | + import errno |
3850 | + if e.errno == errno.ENOENT: |
3851 | + # storage-list does not exist |
3852 | + return [] |
3853 | + raise |
3854 | + |
3855 | + |
3856 | +class UnregisteredHookError(Exception): |
3857 | + """Raised when an undefined hook is called""" |
3858 | + pass |
3859 | + |
3860 | + |
3861 | +class Hooks(object): |
3862 | + """A convenient handler for hook functions. |
3863 | + |
3864 | + Example:: |
3865 | + |
3866 | + hooks = Hooks() |
3867 | + |
3868 | + # register a hook, taking its name from the function name |
3869 | + @hooks.hook() |
3870 | + def install(): |
3871 | + pass # your code here |
3872 | + |
3873 | + # register a hook, providing a custom hook name |
3874 | + @hooks.hook("config-changed") |
3875 | + def config_changed(): |
3876 | + pass # your code here |
3877 | + |
3878 | + if __name__ == "__main__": |
3879 | + # execute a hook based on the name the program is called by |
3880 | + hooks.execute(sys.argv) |
3881 | + """ |
3882 | + |
3883 | + def __init__(self, config_save=None): |
3884 | + super(Hooks, self).__init__() |
3885 | + self._hooks = {} |
3886 | + |
3887 | + # For unknown reasons, we allow the Hooks constructor to override |
3888 | + # config().implicit_save. |
3889 | + if config_save is not None: |
3890 | + config().implicit_save = config_save |
3891 | + |
3892 | + def register(self, name, function): |
3893 | + """Register a hook""" |
3894 | + self._hooks[name] = function |
3895 | + |
3896 | + def execute(self, args): |
3897 | + """Execute a registered hook based on args[0]""" |
3898 | + _run_atstart() |
3899 | + hook_name = os.path.basename(args[0]) |
3900 | + if hook_name in self._hooks: |
3901 | + try: |
3902 | + self._hooks[hook_name]() |
3903 | + except SystemExit as x: |
3904 | + if x.code is None or x.code == 0: |
3905 | + _run_atexit() |
3906 | + raise |
3907 | + _run_atexit() |
3908 | + else: |
3909 | + raise UnregisteredHookError(hook_name) |
3910 | + |
3911 | + def hook(self, *hook_names): |
3912 | + """Decorator, registering them as hooks""" |
3913 | + def wrapper(decorated): |
3914 | + for hook_name in hook_names: |
3915 | + self.register(hook_name, decorated) |
3916 | + else: |
3917 | + self.register(decorated.__name__, decorated) |
3918 | + if '_' in decorated.__name__: |
3919 | + self.register( |
3920 | + decorated.__name__.replace('_', '-'), decorated) |
3921 | + return decorated |
3922 | + return wrapper |
3923 | + |
3924 | + |
3925 | +def charm_dir(): |
3926 | + """Return the root directory of the current charm""" |
3927 | + return os.environ.get('CHARM_DIR') |
3928 | + |
3929 | + |
3930 | +@cached |
3931 | +def action_get(key=None): |
3932 | + """Gets the value of an action parameter, or all key/value param pairs""" |
3933 | + cmd = ['action-get'] |
3934 | + if key is not None: |
3935 | + cmd.append(key) |
3936 | + cmd.append('--format=json') |
3937 | + action_data = json.loads(subprocess.check_output(cmd).decode('UTF-8')) |
3938 | + return action_data |
3939 | + |
3940 | + |
3941 | +def action_set(values): |
3942 | + """Sets the values to be returned after the action finishes""" |
3943 | + cmd = ['action-set'] |
3944 | + for k, v in list(values.items()): |
3945 | + cmd.append('{}={}'.format(k, v)) |
3946 | + subprocess.check_call(cmd) |
3947 | + |
3948 | + |
3949 | +def action_fail(message): |
3950 | + """Sets the action status to failed and sets the error message. |
3951 | + |
3952 | + The results set by action_set are preserved.""" |
3953 | + subprocess.check_call(['action-fail', message]) |
3954 | + |
3955 | + |
3956 | +def action_name(): |
3957 | + """Get the name of the currently executing action.""" |
3958 | + return os.environ.get('JUJU_ACTION_NAME') |
3959 | + |
3960 | + |
3961 | +def action_uuid(): |
3962 | + """Get the UUID of the currently executing action.""" |
3963 | + return os.environ.get('JUJU_ACTION_UUID') |
3964 | + |
3965 | + |
3966 | +def action_tag(): |
3967 | + """Get the tag for the currently executing action.""" |
3968 | + return os.environ.get('JUJU_ACTION_TAG') |
3969 | + |
3970 | + |
3971 | +def status_set(workload_state, message): |
3972 | + """Set the workload state with a message |
3973 | + |
3974 | + Use status-set to set the workload state with a message which is visible |
3975 | + to the user via juju status. If the status-set command is not found then |
3976 | + assume this is juju < 1.23 and juju-log the message unstead. |
3977 | + |
3978 | + workload_state -- valid juju workload state. |
3979 | + message -- status update message |
3980 | + """ |
3981 | + valid_states = ['maintenance', 'blocked', 'waiting', 'active'] |
3982 | + if workload_state not in valid_states: |
3983 | + raise ValueError( |
3984 | + '{!r} is not a valid workload state'.format(workload_state) |
3985 | + ) |
3986 | + cmd = ['status-set', workload_state, message] |
3987 | + try: |
3988 | + ret = subprocess.call(cmd) |
3989 | + if ret == 0: |
3990 | + return |
3991 | + except OSError as e: |
3992 | + if e.errno != errno.ENOENT: |
3993 | + raise |
3994 | + log_message = 'status-set failed: {} {}'.format(workload_state, |
3995 | + message) |
3996 | + log(log_message, level='INFO') |
3997 | + |
3998 | + |
3999 | +def status_get(): |
4000 | + """Retrieve the previously set juju workload state and message |
4001 | + |
4002 | + If the status-get command is not found then assume this is juju < 1.23 and |
4003 | + return 'unknown', "" |
4004 | + |
4005 | + """ |
4006 | + cmd = ['status-get', "--format=json", "--include-data"] |
4007 | + try: |
4008 | + raw_status = subprocess.check_output(cmd) |
4009 | + except OSError as e: |
4010 | + if e.errno == errno.ENOENT: |
4011 | + return ('unknown', "") |
4012 | + else: |
4013 | + raise |
4014 | + else: |
4015 | + status = json.loads(raw_status.decode("UTF-8")) |
4016 | + return (status["status"], status["message"]) |
4017 | + |
4018 | + |
4019 | +def translate_exc(from_exc, to_exc): |
4020 | + def inner_translate_exc1(f): |
4021 | + @wraps(f) |
4022 | + def inner_translate_exc2(*args, **kwargs): |
4023 | + try: |
4024 | + return f(*args, **kwargs) |
4025 | + except from_exc: |
4026 | + raise to_exc |
4027 | + |
4028 | + return inner_translate_exc2 |
4029 | + |
4030 | + return inner_translate_exc1 |
4031 | + |
4032 | + |
4033 | +@translate_exc(from_exc=OSError, to_exc=NotImplementedError) |
4034 | +def is_leader(): |
4035 | + """Does the current unit hold the juju leadership |
4036 | + |
4037 | + Uses juju to determine whether the current unit is the leader of its peers |
4038 | + """ |
4039 | + cmd = ['is-leader', '--format=json'] |
4040 | + return json.loads(subprocess.check_output(cmd).decode('UTF-8')) |
4041 | + |
4042 | + |
4043 | +@translate_exc(from_exc=OSError, to_exc=NotImplementedError) |
4044 | +def leader_get(attribute=None): |
4045 | + """Juju leader get value(s)""" |
4046 | + cmd = ['leader-get', '--format=json'] + [attribute or '-'] |
4047 | + return json.loads(subprocess.check_output(cmd).decode('UTF-8')) |
4048 | + |
4049 | + |
4050 | +@translate_exc(from_exc=OSError, to_exc=NotImplementedError) |
4051 | +def leader_set(settings=None, **kwargs): |
4052 | + """Juju leader set value(s)""" |
4053 | + # Don't log secrets. |
4054 | + # log("Juju leader-set '%s'" % (settings), level=DEBUG) |
4055 | + cmd = ['leader-set'] |
4056 | + settings = settings or {} |
4057 | + settings.update(kwargs) |
4058 | + for k, v in settings.items(): |
4059 | + if v is None: |
4060 | + cmd.append('{}='.format(k)) |
4061 | + else: |
4062 | + cmd.append('{}={}'.format(k, v)) |
4063 | + subprocess.check_call(cmd) |
4064 | + |
4065 | + |
4066 | +@translate_exc(from_exc=OSError, to_exc=NotImplementedError) |
4067 | +def payload_register(ptype, klass, pid): |
4068 | + """ is used while a hook is running to let Juju know that a |
4069 | + payload has been started.""" |
4070 | + cmd = ['payload-register'] |
4071 | + for x in [ptype, klass, pid]: |
4072 | + cmd.append(x) |
4073 | + subprocess.check_call(cmd) |
4074 | + |
4075 | + |
4076 | +@translate_exc(from_exc=OSError, to_exc=NotImplementedError) |
4077 | +def payload_unregister(klass, pid): |
4078 | + """ is used while a hook is running to let Juju know |
4079 | + that a payload has been manually stopped. The <class> and <id> provided |
4080 | + must match a payload that has been previously registered with juju using |
4081 | + payload-register.""" |
4082 | + cmd = ['payload-unregister'] |
4083 | + for x in [klass, pid]: |
4084 | + cmd.append(x) |
4085 | + subprocess.check_call(cmd) |
4086 | + |
4087 | + |
4088 | +@translate_exc(from_exc=OSError, to_exc=NotImplementedError) |
4089 | +def payload_status_set(klass, pid, status): |
4090 | + """is used to update the current status of a registered payload. |
4091 | + The <class> and <id> provided must match a payload that has been previously |
4092 | + registered with juju using payload-register. The <status> must be one of the |
4093 | + follow: starting, started, stopping, stopped""" |
4094 | + cmd = ['payload-status-set'] |
4095 | + for x in [klass, pid, status]: |
4096 | + cmd.append(x) |
4097 | + subprocess.check_call(cmd) |
4098 | + |
4099 | + |
4100 | +@cached |
4101 | +def juju_version(): |
4102 | + """Full version string (eg. '1.23.3.1-trusty-amd64')""" |
4103 | + # Per https://bugs.launchpad.net/juju-core/+bug/1455368/comments/1 |
4104 | + jujud = glob.glob('/var/lib/juju/tools/machine-*/jujud')[0] |
4105 | + return subprocess.check_output([jujud, 'version'], |
4106 | + universal_newlines=True).strip() |
4107 | + |
4108 | + |
4109 | +@cached |
4110 | +def has_juju_version(minimum_version): |
4111 | + """Return True if the Juju version is at least the provided version""" |
4112 | + return LooseVersion(juju_version()) >= LooseVersion(minimum_version) |
4113 | + |
4114 | + |
4115 | +_atexit = [] |
4116 | +_atstart = [] |
4117 | + |
4118 | + |
4119 | +def atstart(callback, *args, **kwargs): |
4120 | + '''Schedule a callback to run before the main hook. |
4121 | + |
4122 | + Callbacks are run in the order they were added. |
4123 | + |
4124 | + This is useful for modules and classes to perform initialization |
4125 | + and inject behavior. In particular: |
4126 | + |
4127 | + - Run common code before all of your hooks, such as logging |
4128 | + the hook name or interesting relation data. |
4129 | + - Defer object or module initialization that requires a hook |
4130 | + context until we know there actually is a hook context, |
4131 | + making testing easier. |
4132 | + - Rather than requiring charm authors to include boilerplate to |
4133 | + invoke your helper's behavior, have it run automatically if |
4134 | + your object is instantiated or module imported. |
4135 | + |
4136 | + This is not at all useful after your hook framework as been launched. |
4137 | + ''' |
4138 | + global _atstart |
4139 | + _atstart.append((callback, args, kwargs)) |
4140 | + |
4141 | + |
4142 | +def atexit(callback, *args, **kwargs): |
4143 | + '''Schedule a callback to run on successful hook completion. |
4144 | + |
4145 | + Callbacks are run in the reverse order that they were added.''' |
4146 | + _atexit.append((callback, args, kwargs)) |
4147 | + |
4148 | + |
4149 | +def _run_atstart(): |
4150 | + '''Hook frameworks must invoke this before running the main hook body.''' |
4151 | + global _atstart |
4152 | + for callback, args, kwargs in _atstart: |
4153 | + callback(*args, **kwargs) |
4154 | + del _atstart[:] |
4155 | + |
4156 | + |
4157 | +def _run_atexit(): |
4158 | + '''Hook frameworks must invoke this after the main hook body has |
4159 | + successfully completed. Do not invoke it if the hook fails.''' |
4160 | + global _atexit |
4161 | + for callback, args, kwargs in reversed(_atexit): |
4162 | + callback(*args, **kwargs) |
4163 | + del _atexit[:] |
4164 | |
4165 | === added file 'charmhelpers/core/host.py' |
4166 | --- charmhelpers/core/host.py 1970-01-01 00:00:00 +0000 |
4167 | +++ charmhelpers/core/host.py 2016-03-10 22:55:31 +0000 |
4168 | @@ -0,0 +1,659 @@ |
4169 | +# Copyright 2014-2015 Canonical Limited. |
4170 | +# |
4171 | +# This file is part of charm-helpers. |
4172 | +# |
4173 | +# charm-helpers is free software: you can redistribute it and/or modify |
4174 | +# it under the terms of the GNU Lesser General Public License version 3 as |
4175 | +# published by the Free Software Foundation. |
4176 | +# |
4177 | +# charm-helpers is distributed in the hope that it will be useful, |
4178 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
4179 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
4180 | +# GNU Lesser General Public License for more details. |
4181 | +# |
4182 | +# You should have received a copy of the GNU Lesser General Public License |
4183 | +# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
4184 | + |
4185 | +"""Tools for working with the host system""" |
4186 | +# Copyright 2012 Canonical Ltd. |
4187 | +# |
4188 | +# Authors: |
4189 | +# Nick Moffitt <nick.moffitt@canonical.com> |
4190 | +# Matthew Wedgwood <matthew.wedgwood@canonical.com> |
4191 | + |
4192 | +import os |
4193 | +import re |
4194 | +import pwd |
4195 | +import glob |
4196 | +import grp |
4197 | +import random |
4198 | +import string |
4199 | +import subprocess |
4200 | +import hashlib |
4201 | +from contextlib import contextmanager |
4202 | +from collections import OrderedDict |
4203 | + |
4204 | +import six |
4205 | + |
4206 | +from .hookenv import log |
4207 | +from .fstab import Fstab |
4208 | + |
4209 | + |
4210 | +def service_start(service_name): |
4211 | + """Start a system service""" |
4212 | + return service('start', service_name) |
4213 | + |
4214 | + |
4215 | +def service_stop(service_name): |
4216 | + """Stop a system service""" |
4217 | + return service('stop', service_name) |
4218 | + |
4219 | + |
4220 | +def service_restart(service_name): |
4221 | + """Restart a system service""" |
4222 | + return service('restart', service_name) |
4223 | + |
4224 | + |
4225 | +def service_reload(service_name, restart_on_failure=False): |
4226 | + """Reload a system service, optionally falling back to restart if |
4227 | + reload fails""" |
4228 | + service_result = service('reload', service_name) |
4229 | + if not service_result and restart_on_failure: |
4230 | + service_result = service('restart', service_name) |
4231 | + return service_result |
4232 | + |
4233 | + |
4234 | +def service_pause(service_name, init_dir="/etc/init", initd_dir="/etc/init.d"): |
4235 | + """Pause a system service. |
4236 | + |
4237 | + Stop it, and prevent it from starting again at boot.""" |
4238 | + stopped = True |
4239 | + if service_running(service_name): |
4240 | + stopped = service_stop(service_name) |
4241 | + upstart_file = os.path.join(init_dir, "{}.conf".format(service_name)) |
4242 | + sysv_file = os.path.join(initd_dir, service_name) |
4243 | + if init_is_systemd(): |
4244 | + service('disable', service_name) |
4245 | + elif os.path.exists(upstart_file): |
4246 | + override_path = os.path.join( |
4247 | + init_dir, '{}.override'.format(service_name)) |
4248 | + with open(override_path, 'w') as fh: |
4249 | + fh.write("manual\n") |
4250 | + elif os.path.exists(sysv_file): |
4251 | + subprocess.check_call(["update-rc.d", service_name, "disable"]) |
4252 | + else: |
4253 | + raise ValueError( |
4254 | + "Unable to detect {0} as SystemD, Upstart {1} or" |
4255 | + " SysV {2}".format( |
4256 | + service_name, upstart_file, sysv_file)) |
4257 | + return stopped |
4258 | + |
4259 | + |
4260 | +def service_resume(service_name, init_dir="/etc/init", |
4261 | + initd_dir="/etc/init.d"): |
4262 | + """Resume a system service. |
4263 | + |
4264 | + Reenable starting again at boot. Start the service""" |
4265 | + upstart_file = os.path.join(init_dir, "{}.conf".format(service_name)) |
4266 | + sysv_file = os.path.join(initd_dir, service_name) |
4267 | + if init_is_systemd(): |
4268 | + service('enable', service_name) |
4269 | + elif os.path.exists(upstart_file): |
4270 | + override_path = os.path.join( |
4271 | + init_dir, '{}.override'.format(service_name)) |
4272 | + if os.path.exists(override_path): |
4273 | + os.unlink(override_path) |
4274 | + elif os.path.exists(sysv_file): |
4275 | + subprocess.check_call(["update-rc.d", service_name, "enable"]) |
4276 | + else: |
4277 | + raise ValueError( |
4278 | + "Unable to detect {0} as SystemD, Upstart {1} or" |
4279 | + " SysV {2}".format( |
4280 | + service_name, upstart_file, sysv_file)) |
4281 | + |
4282 | + started = service_running(service_name) |
4283 | + if not started: |
4284 | + started = service_start(service_name) |
4285 | + return started |
4286 | + |
4287 | + |
4288 | +def service(action, service_name): |
4289 | + """Control a system service""" |
4290 | + if init_is_systemd(): |
4291 | + cmd = ['systemctl', action, service_name] |
4292 | + else: |
4293 | + cmd = ['service', service_name, action] |
4294 | + return subprocess.call(cmd) == 0 |
4295 | + |
4296 | + |
4297 | +def service_running(service_name): |
4298 | + """Determine whether a system service is running""" |
4299 | + if init_is_systemd(): |
4300 | + return service('is-active', service_name) |
4301 | + else: |
4302 | + try: |
4303 | + output = subprocess.check_output( |
4304 | + ['service', service_name, 'status'], |
4305 | + stderr=subprocess.STDOUT).decode('UTF-8') |
4306 | + except subprocess.CalledProcessError: |
4307 | + return False |
4308 | + else: |
4309 | + if ("start/running" in output or "is running" in output or |
4310 | + "up and running" in output): |
4311 | + return True |
4312 | + else: |
4313 | + return False |
4314 | + |
4315 | + |
4316 | +def service_available(service_name): |
4317 | + """Determine whether a system service is available""" |
4318 | + try: |
4319 | + subprocess.check_output( |
4320 | + ['service', service_name, 'status'], |
4321 | + stderr=subprocess.STDOUT).decode('UTF-8') |
4322 | + except subprocess.CalledProcessError as e: |
4323 | + return b'unrecognized service' not in e.output |
4324 | + else: |
4325 | + return True |
4326 | + |
4327 | + |
4328 | +SYSTEMD_SYSTEM = '/run/systemd/system' |
4329 | + |
4330 | + |
4331 | +def init_is_systemd(): |
4332 | + return os.path.isdir(SYSTEMD_SYSTEM) |
4333 | + |
4334 | + |
4335 | +def adduser(username, password=None, shell='/bin/bash', system_user=False, |
4336 | + primary_group=None, secondary_groups=None): |
4337 | + """ |
4338 | + Add a user to the system. |
4339 | + |
4340 | + Will log but otherwise succeed if the user already exists. |
4341 | + |
4342 | + :param str username: Username to create |
4343 | + :param str password: Password for user; if ``None``, create a system user |
4344 | + :param str shell: The default shell for the user |
4345 | + :param bool system_user: Whether to create a login or system user |
4346 | + :param str primary_group: Primary group for user; defaults to their username |
4347 | + :param list secondary_groups: Optional list of additional groups |
4348 | + |
4349 | + :returns: The password database entry struct, as returned by `pwd.getpwnam` |
4350 | + """ |
4351 | + try: |
4352 | + user_info = pwd.getpwnam(username) |
4353 | + log('user {0} already exists!'.format(username)) |
4354 | + except KeyError: |
4355 | + log('creating user {0}'.format(username)) |
4356 | + cmd = ['useradd'] |
4357 | + if system_user or password is None: |
4358 | + cmd.append('--system') |
4359 | + else: |
4360 | + cmd.extend([ |
4361 | + '--create-home', |
4362 | + '--shell', shell, |
4363 | + '--password', password, |
4364 | + ]) |
4365 | + if not primary_group: |
4366 | + try: |
4367 | + grp.getgrnam(username) |
4368 | + primary_group = username # avoid "group exists" error |
4369 | + except KeyError: |
4370 | + pass |
4371 | + if primary_group: |
4372 | + cmd.extend(['-g', primary_group]) |
4373 | + if secondary_groups: |
4374 | + cmd.extend(['-G', ','.join(secondary_groups)]) |
4375 | + cmd.append(username) |
4376 | + subprocess.check_call(cmd) |
4377 | + user_info = pwd.getpwnam(username) |
4378 | + return user_info |
4379 | + |
4380 | + |
4381 | +def user_exists(username): |
4382 | + """Check if a user exists""" |
4383 | + try: |
4384 | + pwd.getpwnam(username) |
4385 | + user_exists = True |
4386 | + except KeyError: |
4387 | + user_exists = False |
4388 | + return user_exists |
4389 | + |
4390 | + |
4391 | +def add_group(group_name, system_group=False): |
4392 | + """Add a group to the system""" |
4393 | + try: |
4394 | + group_info = grp.getgrnam(group_name) |
4395 | + log('group {0} already exists!'.format(group_name)) |
4396 | + except KeyError: |
4397 | + log('creating group {0}'.format(group_name)) |
4398 | + cmd = ['addgroup'] |
4399 | + if system_group: |
4400 | + cmd.append('--system') |
4401 | + else: |
4402 | + cmd.extend([ |
4403 | + '--group', |
4404 | + ]) |
4405 | + cmd.append(group_name) |
4406 | + subprocess.check_call(cmd) |
4407 | + group_info = grp.getgrnam(group_name) |
4408 | + return group_info |
4409 | + |
4410 | + |
4411 | +def add_user_to_group(username, group): |
4412 | + """Add a user to a group""" |
4413 | + cmd = ['gpasswd', '-a', username, group] |
4414 | + log("Adding user {} to group {}".format(username, group)) |
4415 | + subprocess.check_call(cmd) |
4416 | + |
4417 | + |
4418 | +def rsync(from_path, to_path, flags='-r', options=None): |
4419 | + """Replicate the contents of a path""" |
4420 | + options = options or ['--delete', '--executability'] |
4421 | + cmd = ['/usr/bin/rsync', flags] |
4422 | + cmd.extend(options) |
4423 | + cmd.append(from_path) |
4424 | + cmd.append(to_path) |
4425 | + log(" ".join(cmd)) |
4426 | + return subprocess.check_output(cmd).decode('UTF-8').strip() |
4427 | + |
4428 | + |
4429 | +def symlink(source, destination): |
4430 | + """Create a symbolic link""" |
4431 | + log("Symlinking {} as {}".format(source, destination)) |
4432 | + cmd = [ |
4433 | + 'ln', |
4434 | + '-sf', |
4435 | + source, |
4436 | + destination, |
4437 | + ] |
4438 | + subprocess.check_call(cmd) |
4439 | + |
4440 | + |
4441 | +def mkdir(path, owner='root', group='root', perms=0o555, force=False): |
4442 | + """Create a directory""" |
4443 | + log("Making dir {} {}:{} {:o}".format(path, owner, group, |
4444 | + perms)) |
4445 | + uid = pwd.getpwnam(owner).pw_uid |
4446 | + gid = grp.getgrnam(group).gr_gid |
4447 | + realpath = os.path.abspath(path) |
4448 | + path_exists = os.path.exists(realpath) |
4449 | + if path_exists and force: |
4450 | + if not os.path.isdir(realpath): |
4451 | + log("Removing non-directory file {} prior to mkdir()".format(path)) |
4452 | + os.unlink(realpath) |
4453 | + os.makedirs(realpath, perms) |
4454 | + elif not path_exists: |
4455 | + os.makedirs(realpath, perms) |
4456 | + os.chown(realpath, uid, gid) |
4457 | + os.chmod(realpath, perms) |
4458 | + |
4459 | + |
4460 | +def write_file(path, content, owner='root', group='root', perms=0o444): |
4461 | + """Create or overwrite a file with the contents of a byte string.""" |
4462 | + log("Writing file {} {}:{} {:o}".format(path, owner, group, perms)) |
4463 | + uid = pwd.getpwnam(owner).pw_uid |
4464 | + gid = grp.getgrnam(group).gr_gid |
4465 | + with open(path, 'wb') as target: |
4466 | + os.fchown(target.fileno(), uid, gid) |
4467 | + os.fchmod(target.fileno(), perms) |
4468 | + target.write(content) |
4469 | + |
4470 | + |
4471 | +def fstab_remove(mp): |
4472 | + """Remove the given mountpoint entry from /etc/fstab |
4473 | + """ |
4474 | + return Fstab.remove_by_mountpoint(mp) |
4475 | + |
4476 | + |
4477 | +def fstab_add(dev, mp, fs, options=None): |
4478 | + """Adds the given device entry to the /etc/fstab file |
4479 | + """ |
4480 | + return Fstab.add(dev, mp, fs, options=options) |
4481 | + |
4482 | + |
4483 | +def mount(device, mountpoint, options=None, persist=False, filesystem="ext3"): |
4484 | + """Mount a filesystem at a particular mountpoint""" |
4485 | + cmd_args = ['mount'] |
4486 | + if options is not None: |
4487 | + cmd_args.extend(['-o', options]) |
4488 | + cmd_args.extend([device, mountpoint]) |
4489 | + try: |
4490 | + subprocess.check_output(cmd_args) |
4491 | + except subprocess.CalledProcessError as e: |
4492 | + log('Error mounting {} at {}\n{}'.format(device, mountpoint, e.output)) |
4493 | + return False |
4494 | + |
4495 | + if persist: |
4496 | + return fstab_add(device, mountpoint, filesystem, options=options) |
4497 | + return True |
4498 | + |
4499 | + |
4500 | +def umount(mountpoint, persist=False): |
4501 | + """Unmount a filesystem""" |
4502 | + cmd_args = ['umount', mountpoint] |
4503 | + try: |
4504 | + subprocess.check_output(cmd_args) |
4505 | + except subprocess.CalledProcessError as e: |
4506 | + log('Error unmounting {}\n{}'.format(mountpoint, e.output)) |
4507 | + return False |
4508 | + |
4509 | + if persist: |
4510 | + return fstab_remove(mountpoint) |
4511 | + return True |
4512 | + |
4513 | + |
4514 | +def mounts(): |
4515 | + """Get a list of all mounted volumes as [[mountpoint,device],[...]]""" |
4516 | + with open('/proc/mounts') as f: |
4517 | + # [['/mount/point','/dev/path'],[...]] |
4518 | + system_mounts = [m[1::-1] for m in [l.strip().split() |
4519 | + for l in f.readlines()]] |
4520 | + return system_mounts |
4521 | + |
4522 | + |
4523 | +def fstab_mount(mountpoint): |
4524 | + """Mount filesystem using fstab""" |
4525 | + cmd_args = ['mount', mountpoint] |
4526 | + try: |
4527 | + subprocess.check_output(cmd_args) |
4528 | + except subprocess.CalledProcessError as e: |
4529 | + log('Error unmounting {}\n{}'.format(mountpoint, e.output)) |
4530 | + return False |
4531 | + return True |
4532 | + |
4533 | + |
4534 | +def file_hash(path, hash_type='md5'): |
4535 | + """ |
4536 | + Generate a hash checksum of the contents of 'path' or None if not found. |
4537 | + |
4538 | + :param str hash_type: Any hash alrgorithm supported by :mod:`hashlib`, |
4539 | + such as md5, sha1, sha256, sha512, etc. |
4540 | + """ |
4541 | + if os.path.exists(path): |
4542 | + h = getattr(hashlib, hash_type)() |
4543 | + with open(path, 'rb') as source: |
4544 | + h.update(source.read()) |
4545 | + return h.hexdigest() |
4546 | + else: |
4547 | + return None |
4548 | + |
4549 | + |
4550 | +def path_hash(path): |
4551 | + """ |
4552 | + Generate a hash checksum of all files matching 'path'. Standard wildcards |
4553 | + like '*' and '?' are supported, see documentation for the 'glob' module for |
4554 | + more information. |
4555 | + |
4556 | + :return: dict: A { filename: hash } dictionary for all matched files. |
4557 | + Empty if none found. |
4558 | + """ |
4559 | + return { |
4560 | + filename: file_hash(filename) |
4561 | + for filename in glob.iglob(path) |
4562 | + } |
4563 | + |
4564 | + |
4565 | +def check_hash(path, checksum, hash_type='md5'): |
4566 | + """ |
4567 | + Validate a file using a cryptographic checksum. |
4568 | + |
4569 | + :param str checksum: Value of the checksum used to validate the file. |
4570 | + :param str hash_type: Hash algorithm used to generate `checksum`. |
4571 | + Can be any hash alrgorithm supported by :mod:`hashlib`, |
4572 | + such as md5, sha1, sha256, sha512, etc. |
4573 | + :raises ChecksumError: If the file fails the checksum |
4574 | + |
4575 | + """ |
4576 | + actual_checksum = file_hash(path, hash_type) |
4577 | + if checksum != actual_checksum: |
4578 | + raise ChecksumError("'%s' != '%s'" % (checksum, actual_checksum)) |
4579 | + |
4580 | + |
4581 | +class ChecksumError(ValueError): |
4582 | + pass |
4583 | + |
4584 | + |
4585 | +def restart_on_change(restart_map, stopstart=False): |
4586 | + """Restart services based on configuration files changing |
4587 | + |
4588 | + This function is used a decorator, for example:: |
4589 | + |
4590 | + @restart_on_change({ |
4591 | + '/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ] |
4592 | + '/etc/apache/sites-enabled/*': [ 'apache2' ] |
4593 | + }) |
4594 | + def config_changed(): |
4595 | + pass # your code here |
4596 | + |
4597 | + In this example, the cinder-api and cinder-volume services |
4598 | + would be restarted if /etc/ceph/ceph.conf is changed by the |
4599 | + ceph_client_changed function. The apache2 service would be |
4600 | + restarted if any file matching the pattern got changed, created |
4601 | + or removed. Standard wildcards are supported, see documentation |
4602 | + for the 'glob' module for more information. |
4603 | + """ |
4604 | + def wrap(f): |
4605 | + def wrapped_f(*args, **kwargs): |
4606 | + checksums = {path: path_hash(path) for path in restart_map} |
4607 | + f(*args, **kwargs) |
4608 | + restarts = [] |
4609 | + for path in restart_map: |
4610 | + if path_hash(path) != checksums[path]: |
4611 | + restarts += restart_map[path] |
4612 | + services_list = list(OrderedDict.fromkeys(restarts)) |
4613 | + if not stopstart: |
4614 | + for service_name in services_list: |
4615 | + service('restart', service_name) |
4616 | + else: |
4617 | + for action in ['stop', 'start']: |
4618 | + for service_name in services_list: |
4619 | + service(action, service_name) |
4620 | + return wrapped_f |
4621 | + return wrap |
4622 | + |
4623 | + |
4624 | +def lsb_release(): |
4625 | + """Return /etc/lsb-release in a dict""" |
4626 | + d = {} |
4627 | + with open('/etc/lsb-release', 'r') as lsb: |
4628 | + for l in lsb: |
4629 | + k, v = l.split('=') |
4630 | + d[k.strip()] = v.strip() |
4631 | + return d |
4632 | + |
4633 | + |
4634 | +def pwgen(length=None): |
4635 | + """Generate a random pasword.""" |
4636 | + if length is None: |
4637 | + # A random length is ok to use a weak PRNG |
4638 | + length = random.choice(range(35, 45)) |
4639 | + alphanumeric_chars = [ |
4640 | + l for l in (string.ascii_letters + string.digits) |
4641 | + if l not in 'l0QD1vAEIOUaeiou'] |
4642 | + # Use a crypto-friendly PRNG (e.g. /dev/urandom) for making the |
4643 | + # actual password |
4644 | + random_generator = random.SystemRandom() |
4645 | + random_chars = [ |
4646 | + random_generator.choice(alphanumeric_chars) for _ in range(length)] |
4647 | + return(''.join(random_chars)) |
4648 | + |
4649 | + |
4650 | +def is_phy_iface(interface): |
4651 | + """Returns True if interface is not virtual, otherwise False.""" |
4652 | + if interface: |
4653 | + sys_net = '/sys/class/net' |
4654 | + if os.path.isdir(sys_net): |
4655 | + for iface in glob.glob(os.path.join(sys_net, '*')): |
4656 | + if '/virtual/' in os.path.realpath(iface): |
4657 | + continue |
4658 | + |
4659 | + if interface == os.path.basename(iface): |
4660 | + return True |
4661 | + |
4662 | + return False |
4663 | + |
4664 | + |
4665 | +def get_bond_master(interface): |
4666 | + """Returns bond master if interface is bond slave otherwise None. |
4667 | + |
4668 | + NOTE: the provided interface is expected to be physical |
4669 | + """ |
4670 | + if interface: |
4671 | + iface_path = '/sys/class/net/%s' % (interface) |
4672 | + if os.path.exists(iface_path): |
4673 | + if '/virtual/' in os.path.realpath(iface_path): |
4674 | + return None |
4675 | + |
4676 | + master = os.path.join(iface_path, 'master') |
4677 | + if os.path.exists(master): |
4678 | + master = os.path.realpath(master) |
4679 | + # make sure it is a bond master |
4680 | + if os.path.exists(os.path.join(master, 'bonding')): |
4681 | + return os.path.basename(master) |
4682 | + |
4683 | + return None |
4684 | + |
4685 | + |
4686 | +def list_nics(nic_type=None): |
4687 | + '''Return a list of nics of given type(s)''' |
4688 | + if isinstance(nic_type, six.string_types): |
4689 | + int_types = [nic_type] |
4690 | + else: |
4691 | + int_types = nic_type |
4692 | + |
4693 | + interfaces = [] |
4694 | + if nic_type: |
4695 | + for int_type in int_types: |
4696 | + cmd = ['ip', 'addr', 'show', 'label', int_type + '*'] |
4697 | + ip_output = subprocess.check_output(cmd).decode('UTF-8') |
4698 | + ip_output = ip_output.split('\n') |
4699 | + ip_output = (line for line in ip_output if line) |
4700 | + for line in ip_output: |
4701 | + if line.split()[1].startswith(int_type): |
4702 | + matched = re.search('.*: (' + int_type + |
4703 | + r'[0-9]+\.[0-9]+)@.*', line) |
4704 | + if matched: |
4705 | + iface = matched.groups()[0] |
4706 | + else: |
4707 | + iface = line.split()[1].replace(":", "") |
4708 | + |
4709 | + if iface not in interfaces: |
4710 | + interfaces.append(iface) |
4711 | + else: |
4712 | + cmd = ['ip', 'a'] |
4713 | + ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n') |
4714 | + ip_output = (line.strip() for line in ip_output if line) |
4715 | + |
4716 | + key = re.compile('^[0-9]+:\s+(.+):') |
4717 | + for line in ip_output: |
4718 | + matched = re.search(key, line) |
4719 | + if matched: |
4720 | + iface = matched.group(1) |
4721 | + iface = iface.partition("@")[0] |
4722 | + if iface not in interfaces: |
4723 | + interfaces.append(iface) |
4724 | + |
4725 | + return interfaces |
4726 | + |
4727 | + |
4728 | +def set_nic_mtu(nic, mtu): |
4729 | + '''Set MTU on a network interface''' |
4730 | + cmd = ['ip', 'link', 'set', nic, 'mtu', mtu] |
4731 | + subprocess.check_call(cmd) |
4732 | + |
4733 | + |
4734 | +def get_nic_mtu(nic): |
4735 | + cmd = ['ip', 'addr', 'show', nic] |
4736 | + ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n') |
4737 | + mtu = "" |
4738 | + for line in ip_output: |
4739 | + words = line.split() |
4740 | + if 'mtu' in words: |
4741 | + mtu = words[words.index("mtu") + 1] |
4742 | + return mtu |
4743 | + |
4744 | + |
4745 | +def get_nic_hwaddr(nic): |
4746 | + cmd = ['ip', '-o', '-0', 'addr', 'show', nic] |
4747 | + ip_output = subprocess.check_output(cmd).decode('UTF-8') |
4748 | + hwaddr = "" |
4749 | + words = ip_output.split() |
4750 | + if 'link/ether' in words: |
4751 | + hwaddr = words[words.index('link/ether') + 1] |
4752 | + return hwaddr |
4753 | + |
4754 | + |
4755 | +def cmp_pkgrevno(package, revno, pkgcache=None): |
4756 | + '''Compare supplied revno with the revno of the installed package |
4757 | + |
4758 | + * 1 => Installed revno is greater than supplied arg |
4759 | + * 0 => Installed revno is the same as supplied arg |
4760 | + * -1 => Installed revno is less than supplied arg |
4761 | + |
4762 | + This function imports apt_cache function from charmhelpers.fetch if |
4763 | + the pkgcache argument is None. Be sure to add charmhelpers.fetch if |
4764 | + you call this function, or pass an apt_pkg.Cache() instance. |
4765 | + ''' |
4766 | + import apt_pkg |
4767 | + if not pkgcache: |
4768 | + from charmhelpers.fetch import apt_cache |
4769 | + pkgcache = apt_cache() |
4770 | + pkg = pkgcache[package] |
4771 | + return apt_pkg.version_compare(pkg.current_ver.ver_str, revno) |
4772 | + |
4773 | + |
4774 | +@contextmanager |
4775 | +def chdir(d): |
4776 | + cur = os.getcwd() |
4777 | + try: |
4778 | + yield os.chdir(d) |
4779 | + finally: |
4780 | + os.chdir(cur) |
4781 | + |
4782 | + |
4783 | +def chownr(path, owner, group, follow_links=True, chowntopdir=False): |
4784 | + """ |
4785 | + Recursively change user and group ownership of files and directories |
4786 | + in given path. Doesn't chown path itself by default, only its children. |
4787 | + |
4788 | + :param bool follow_links: Also Chown links if True |
4789 | + :param bool chowntopdir: Also chown path itself if True |
4790 | + """ |
4791 | + uid = pwd.getpwnam(owner).pw_uid |
4792 | + gid = grp.getgrnam(group).gr_gid |
4793 | + if follow_links: |
4794 | + chown = os.chown |
4795 | + else: |
4796 | + chown = os.lchown |
4797 | + |
4798 | + if chowntopdir: |
4799 | + broken_symlink = os.path.lexists(path) and not os.path.exists(path) |
4800 | + if not broken_symlink: |
4801 | + chown(path, uid, gid) |
4802 | + for root, dirs, files in os.walk(path): |
4803 | + for name in dirs + files: |
4804 | + full = os.path.join(root, name) |
4805 | + broken_symlink = os.path.lexists(full) and not os.path.exists(full) |
4806 | + if not broken_symlink: |
4807 | + chown(full, uid, gid) |
4808 | + |
4809 | + |
4810 | +def lchownr(path, owner, group): |
4811 | + chownr(path, owner, group, follow_links=False) |
4812 | + |
4813 | + |
4814 | +def get_total_ram(): |
4815 | + '''The total amount of system RAM in bytes. |
4816 | + |
4817 | + This is what is reported by the OS, and may be overcommitted when |
4818 | + there are multiple containers hosted on the same machine. |
4819 | + ''' |
4820 | + with open('/proc/meminfo', 'r') as f: |
4821 | + for line in f.readlines(): |
4822 | + if line: |
4823 | + key, value, unit = line.split() |
4824 | + if key == 'MemTotal:': |
4825 | + assert unit == 'kB', 'Unknown unit' |
4826 | + return int(value) * 1024 # Classic, not KiB. |
4827 | + raise NotImplementedError() |
4828 | |
4829 | === added file 'charmhelpers/core/hugepage.py' |
4830 | --- charmhelpers/core/hugepage.py 1970-01-01 00:00:00 +0000 |
4831 | +++ charmhelpers/core/hugepage.py 2016-03-10 22:55:31 +0000 |
4832 | @@ -0,0 +1,71 @@ |
4833 | +# -*- coding: utf-8 -*- |
4834 | + |
4835 | +# Copyright 2014-2015 Canonical Limited. |
4836 | +# |
4837 | +# This file is part of charm-helpers. |
4838 | +# |
4839 | +# charm-helpers is free software: you can redistribute it and/or modify |
4840 | +# it under the terms of the GNU Lesser General Public License version 3 as |
4841 | +# published by the Free Software Foundation. |
4842 | +# |
4843 | +# charm-helpers is distributed in the hope that it will be useful, |
4844 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
4845 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
4846 | +# GNU Lesser General Public License for more details. |
4847 | +# |
4848 | +# You should have received a copy of the GNU Lesser General Public License |
4849 | +# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
4850 | + |
4851 | +import yaml |
4852 | +from charmhelpers.core import fstab |
4853 | +from charmhelpers.core import sysctl |
4854 | +from charmhelpers.core.host import ( |
4855 | + add_group, |
4856 | + add_user_to_group, |
4857 | + fstab_mount, |
4858 | + mkdir, |
4859 | +) |
4860 | +from charmhelpers.core.strutils import bytes_from_string |
4861 | +from subprocess import check_output |
4862 | + |
4863 | + |
4864 | +def hugepage_support(user, group='hugetlb', nr_hugepages=256, |
4865 | + max_map_count=65536, mnt_point='/run/hugepages/kvm', |
4866 | + pagesize='2MB', mount=True, set_shmmax=False): |
4867 | + """Enable hugepages on system. |
4868 | + |
4869 | + Args: |
4870 | + user (str) -- Username to allow access to hugepages to |
4871 | + group (str) -- Group name to own hugepages |
4872 | + nr_hugepages (int) -- Number of pages to reserve |
4873 | + max_map_count (int) -- Number of Virtual Memory Areas a process can own |
4874 | + mnt_point (str) -- Directory to mount hugepages on |
4875 | + pagesize (str) -- Size of hugepages |
4876 | + mount (bool) -- Whether to Mount hugepages |
4877 | + """ |
4878 | + group_info = add_group(group) |
4879 | + gid = group_info.gr_gid |
4880 | + add_user_to_group(user, group) |
4881 | + if max_map_count < 2 * nr_hugepages: |
4882 | + max_map_count = 2 * nr_hugepages |
4883 | + sysctl_settings = { |
4884 | + 'vm.nr_hugepages': nr_hugepages, |
4885 | + 'vm.max_map_count': max_map_count, |
4886 | + 'vm.hugetlb_shm_group': gid, |
4887 | + } |
4888 | + if set_shmmax: |
4889 | + shmmax_current = int(check_output(['sysctl', '-n', 'kernel.shmmax'])) |
4890 | + shmmax_minsize = bytes_from_string(pagesize) * nr_hugepages |
4891 | + if shmmax_minsize > shmmax_current: |
4892 | + sysctl_settings['kernel.shmmax'] = shmmax_minsize |
4893 | + sysctl.create(yaml.dump(sysctl_settings), '/etc/sysctl.d/10-hugepage.conf') |
4894 | + mkdir(mnt_point, owner='root', group='root', perms=0o755, force=False) |
4895 | + lfstab = fstab.Fstab() |
4896 | + fstab_entry = lfstab.get_entry_by_attr('mountpoint', mnt_point) |
4897 | + if fstab_entry: |
4898 | + lfstab.remove_entry(fstab_entry) |
4899 | + entry = lfstab.Entry('nodev', mnt_point, 'hugetlbfs', |
4900 | + 'mode=1770,gid={},pagesize={}'.format(gid, pagesize), 0, 0) |
4901 | + lfstab.add_entry(entry) |
4902 | + if mount: |
4903 | + fstab_mount(mnt_point) |
4904 | |
4905 | === added file 'charmhelpers/core/kernel.py' |
4906 | --- charmhelpers/core/kernel.py 1970-01-01 00:00:00 +0000 |
4907 | +++ charmhelpers/core/kernel.py 2016-03-10 22:55:31 +0000 |
4908 | @@ -0,0 +1,68 @@ |
4909 | +#!/usr/bin/env python |
4910 | +# -*- coding: utf-8 -*- |
4911 | + |
4912 | +# Copyright 2014-2015 Canonical Limited. |
4913 | +# |
4914 | +# This file is part of charm-helpers. |
4915 | +# |
4916 | +# charm-helpers is free software: you can redistribute it and/or modify |
4917 | +# it under the terms of the GNU Lesser General Public License version 3 as |
4918 | +# published by the Free Software Foundation. |
4919 | +# |
4920 | +# charm-helpers is distributed in the hope that it will be useful, |
4921 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
4922 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
4923 | +# GNU Lesser General Public License for more details. |
4924 | +# |
4925 | +# You should have received a copy of the GNU Lesser General Public License |
4926 | +# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
4927 | + |
4928 | +__author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>" |
4929 | + |
4930 | +from charmhelpers.core.hookenv import ( |
4931 | + log, |
4932 | + INFO |
4933 | +) |
4934 | + |
4935 | +from subprocess import check_call, check_output |
4936 | +import re |
4937 | + |
4938 | + |
4939 | +def modprobe(module, persist=True): |
4940 | + """Load a kernel module and configure for auto-load on reboot.""" |
4941 | + cmd = ['modprobe', module] |
4942 | + |
4943 | + log('Loading kernel module %s' % module, level=INFO) |
4944 | + |
4945 | + check_call(cmd) |
4946 | + if persist: |
4947 | + with open('/etc/modules', 'r+') as modules: |
4948 | + if module not in modules.read(): |
4949 | + modules.write(module) |
4950 | + |
4951 | + |
4952 | +def rmmod(module, force=False): |
4953 | + """Remove a module from the linux kernel""" |
4954 | + cmd = ['rmmod'] |
4955 | + if force: |
4956 | + cmd.append('-f') |
4957 | + cmd.append(module) |
4958 | + log('Removing kernel module %s' % module, level=INFO) |
4959 | + return check_call(cmd) |
4960 | + |
4961 | + |
4962 | +def lsmod(): |
4963 | + """Shows what kernel modules are currently loaded""" |
4964 | + return check_output(['lsmod'], |
4965 | + universal_newlines=True) |
4966 | + |
4967 | + |
4968 | +def is_module_loaded(module): |
4969 | + """Checks if a kernel module is already loaded""" |
4970 | + matches = re.findall('^%s[ ]+' % module, lsmod(), re.M) |
4971 | + return len(matches) > 0 |
4972 | + |
4973 | + |
4974 | +def update_initramfs(version='all'): |
4975 | + """Updates an initramfs image""" |
4976 | + return check_call(["update-initramfs", "-k", version, "-u"]) |
4977 | |
4978 | === added directory 'charmhelpers/core/services' |
4979 | === added file 'charmhelpers/core/services/__init__.py' |
4980 | --- charmhelpers/core/services/__init__.py 1970-01-01 00:00:00 +0000 |
4981 | +++ charmhelpers/core/services/__init__.py 2016-03-10 22:55:31 +0000 |
4982 | @@ -0,0 +1,18 @@ |
4983 | +# Copyright 2014-2015 Canonical Limited. |
4984 | +# |
4985 | +# This file is part of charm-helpers. |
4986 | +# |
4987 | +# charm-helpers is free software: you can redistribute it and/or modify |
4988 | +# it under the terms of the GNU Lesser General Public License version 3 as |
4989 | +# published by the Free Software Foundation. |
4990 | +# |
4991 | +# charm-helpers is distributed in the hope that it will be useful, |
4992 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
4993 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
4994 | +# GNU Lesser General Public License for more details. |
4995 | +# |
4996 | +# You should have received a copy of the GNU Lesser General Public License |
4997 | +# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
4998 | + |
4999 | +from .base import * # NOQA |
5000 | +from .helpers import * # NOQA |