Merge lp:~jjo/charms/trusty/ubuntu/add-execd-and-charmhelpers-syncing into lp:charms/trusty/ubuntu

Proposed by JuanJo Ciarlante
Status: Work in progress
Proposed branch: lp:~jjo/charms/trusty/ubuntu/add-execd-and-charmhelpers-syncing
Merge into: lp:charms/trusty/ubuntu
Diff against target: 3424 lines (+3305/-0)
21 files modified
Makefile (+1/-0)
charm-helpers-hooks.yaml (+5/-0)
exec.d/README.md (+25/-0)
hooks/charmhelpers/__init__.py (+38/-0)
hooks/charmhelpers/core/__init__.py (+15/-0)
hooks/charmhelpers/core/decorators.py (+57/-0)
hooks/charmhelpers/core/files.py (+45/-0)
hooks/charmhelpers/core/fstab.py (+134/-0)
hooks/charmhelpers/core/hookenv.py (+898/-0)
hooks/charmhelpers/core/host.py (+570/-0)
hooks/charmhelpers/core/hugepage.py (+62/-0)
hooks/charmhelpers/core/services/__init__.py (+18/-0)
hooks/charmhelpers/core/services/base.py (+353/-0)
hooks/charmhelpers/core/services/helpers.py (+283/-0)
hooks/charmhelpers/core/strutils.py (+42/-0)
hooks/charmhelpers/core/sysctl.py (+56/-0)
hooks/charmhelpers/core/templating.py (+68/-0)
hooks/charmhelpers/core/unitdata.py (+521/-0)
hooks/charmhelpers/payload/__init__.py (+17/-0)
hooks/charmhelpers/payload/execd.py (+66/-0)
hooks/hooks.py (+31/-0)
To merge this branch: bzr merge lp:~jjo/charms/trusty/ubuntu/add-execd-and-charmhelpers-syncing
Reviewer Review Type Date Requested Status
JuanJo Ciarlante (community) withdrawn Disapprove
Ryan Beisner (community) Needs Resubmitting
Tim Van Steenburgh (community) Needs Fixing
Whit Morriss (community) Approve
Review via email: mp+256136@code.launchpad.net
To post a comment you must log in.
Revision history for this message
JuanJo Ciarlante (jjo) wrote :

FYI my real hooks changes (besides charmhelpers sync):
- hooks.py: added install()
- ln -sf hooks.py install
- ln -sf hooks.py config-changed (config-changed and hooks.py were dup'd)

Revision history for this message
Marco Ceppi (marcoceppi) wrote :

Why? This seems to really be stretching the concept of what the juju charm was meant to do.

Revision history for this message
Marco Ceppi (marcoceppi) wrote :

Why? This seems to really be stretching the concept of what the juju charm was meant to do.

Revision history for this message
JuanJo Ciarlante (jjo) wrote :

exec.d mechanism (as indeed present at charmhelpers) is very useful for doing stuff pre-install hook run, in Canonical IS we use it to setup private archives, ntp, etc - fwiw the lxcbr0 hack already present at this charm already stretches its "dummy" purpose.

Revision history for this message
Whit Morriss (whitmo) wrote :

Generally LGTM +1. Merges cleanly, tests pass.

I don't have a problem with the intent, making the charm more useful is great. Bundletester fails attempting to run tests and lint from Makefile due to dependency issues, but passes on the same tests later (should not block, should be noted for cleanup). Lint passes once flake8 is installed locally.

review: Approve
Revision history for this message
Marco Ceppi (marcoceppi) wrote :

Hey jjo, it seems that this now conflicts like crazy on merge. Could you clean up before we promulgate?

Revision history for this message
Tim Van Steenburgh (tvansteenburgh) wrote :

Waiting for merge conflicts to be resolved.

review: Needs Fixing
Revision history for this message
Adam Israel (aisrael) wrote :

Hey jjo,

I switched this back to work in progress. Please switch it back to Needs Review once the merge conflicts have been resolved.

Thanks!

Revision history for this message
JuanJo Ciarlante (jjo) wrote :

Thanks @aisrael, I've synced exec.d/ addition to current trunk, PTAL.

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_lint_check #8991 ubuntu for jjo mp256136
    LINT OK: passed

Build: http://10.245.162.77:8080/job/charm_lint_check/8991/

14. By JuanJo Ciarlante

[jjo] add exec.d/README (which also serves at placeholder, to have exec.d/ directory ready to be used)

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_lint_check #9041 ubuntu for jjo mp256136
    LINT OK: passed

Build: http://10.245.162.77:8080/job/charm_lint_check/9041/

Revision history for this message
Ryan Beisner (1chb1n) wrote :

Please hold the merge, pending validation of amulet tests in uosci. Thank you.

review: Needs Information
Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_amulet_test #6145 ubuntu for jjo mp256136
    AMULET OK: passed

Build: http://10.245.162.77:8080/job/charm_amulet_test/6145/

Revision history for this message
Ryan Beisner (1chb1n) wrote :

Thank you for your work on this.

To-date, the Ubuntu charm has been maintained strictly as a hookless, ultra-simplistic charm.

## UNIT TESTS
With this being the first python code in the charm, I think that unit tests should also accompany those changes, even as simple as they are.

For example, the execd_preinstall() helper is represented in the cinder and neutron-api charms as install hook unit tests. Something similar should be done here so that reviewers of this proposal, and future reviewers, will have that to lean on.

## AMULET TESTS
The amulet test passes for Precise, Trusty, Vivid and Wily, indicating that the proposed changes do not impact basic functionality of the Ubuntu charm.

review: Needs Resubmitting
Revision history for this message
JuanJo Ciarlante (jjo) wrote :

Agreed on this being too invasive for a ~dummy charm - withdrawing.

review: Disapprove (withdrawn)

Unmerged revisions

14. By JuanJo Ciarlante

[jjo] add exec.d/README (which also serves at placeholder, to have exec.d/ directory ready to be used)

13. By JuanJo Ciarlante

[jjo] add exec.d/ support using charmhelpers execd_preinstall()

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'Makefile'
2--- Makefile 2015-04-30 13:14:03 +0000
3+++ Makefile 2015-08-30 15:58:19 +0000
4@@ -47,6 +47,7 @@
5 sync: bin/charm_helpers_sync.py
6 @echo Syncing charm helpers for functional tests...
7 @$(PYTHON) bin/charm_helpers_sync.py -c charm-helpers-tests.yaml
8+ @$(PYTHON) bin/charm_helpers_sync.py -c charm-helpers-hooks.yaml
9
10 publish: clean lint
11 bzr push lp:charms/trusty/ubuntu
12
13=== added file 'charm-helpers-hooks.yaml'
14--- charm-helpers-hooks.yaml 1970-01-01 00:00:00 +0000
15+++ charm-helpers-hooks.yaml 2015-08-30 15:58:19 +0000
16@@ -0,0 +1,5 @@
17+branch: lp:charm-helpers
18+destination: hooks/charmhelpers
19+include:
20+ - core
21+ - payload.execd
22
23=== added directory 'exec.d'
24=== added file 'exec.d/README.md'
25--- exec.d/README.md 1970-01-01 00:00:00 +0000
26+++ exec.d/README.md 2015-08-30 15:58:19 +0000
27@@ -0,0 +1,25 @@
28+# Using exec.d/
29+
30+Early in the install hook, this charm will run every executable file found at:
31+
32+ exec.d/*/charm-pre-install
33+
34+Typical use cases cases are: overriding default repositories and/or adding custom ones,
35+specific /etc/ setup, and alike.
36+
37+For example, to add a custom apt-repository:
38+
39+ mkdir -p exec.d/add-my-repo
40+ echo "apt-get install python-software-properties" > exec.d/add-my-repo/charm-pre-install
41+ echo "add-apt-repository ppa:some-ppa" >> exec.d/add-my-repo/charm-pre-install
42+ chmod +x exec.d/add-my-repo/charm-pre-install
43+
44+This layout intends to be VCS-friendly, for the example above, if you
45+kept taht specific initialization under VCS control (such that
46+charm-pre-install executable file is at branch's root), you could do:
47+
48+ bzr branch lp:~user/someproject/add-my-repo exec.d/add-my-repo
49+
50+or e.g.:
51+
52+ git clone git://mygitserver/user/add-my-repo exec.d/add-my-repo
53
54=== added directory 'hooks'
55=== added directory 'hooks/charmhelpers'
56=== added file 'hooks/charmhelpers/__init__.py'
57--- hooks/charmhelpers/__init__.py 1970-01-01 00:00:00 +0000
58+++ hooks/charmhelpers/__init__.py 2015-08-30 15:58:19 +0000
59@@ -0,0 +1,38 @@
60+# Copyright 2014-2015 Canonical Limited.
61+#
62+# This file is part of charm-helpers.
63+#
64+# charm-helpers is free software: you can redistribute it and/or modify
65+# it under the terms of the GNU Lesser General Public License version 3 as
66+# published by the Free Software Foundation.
67+#
68+# charm-helpers is distributed in the hope that it will be useful,
69+# but WITHOUT ANY WARRANTY; without even the implied warranty of
70+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
71+# GNU Lesser General Public License for more details.
72+#
73+# You should have received a copy of the GNU Lesser General Public License
74+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
75+
76+# Bootstrap charm-helpers, installing its dependencies if necessary using
77+# only standard libraries.
78+import subprocess
79+import sys
80+
81+try:
82+ import six # flake8: noqa
83+except ImportError:
84+ if sys.version_info.major == 2:
85+ subprocess.check_call(['apt-get', 'install', '-y', 'python-six'])
86+ else:
87+ subprocess.check_call(['apt-get', 'install', '-y', 'python3-six'])
88+ import six # flake8: noqa
89+
90+try:
91+ import yaml # flake8: noqa
92+except ImportError:
93+ if sys.version_info.major == 2:
94+ subprocess.check_call(['apt-get', 'install', '-y', 'python-yaml'])
95+ else:
96+ subprocess.check_call(['apt-get', 'install', '-y', 'python3-yaml'])
97+ import yaml # flake8: noqa
98
99=== added directory 'hooks/charmhelpers/core'
100=== added file 'hooks/charmhelpers/core/__init__.py'
101--- hooks/charmhelpers/core/__init__.py 1970-01-01 00:00:00 +0000
102+++ hooks/charmhelpers/core/__init__.py 2015-08-30 15:58:19 +0000
103@@ -0,0 +1,15 @@
104+# Copyright 2014-2015 Canonical Limited.
105+#
106+# This file is part of charm-helpers.
107+#
108+# charm-helpers is free software: you can redistribute it and/or modify
109+# it under the terms of the GNU Lesser General Public License version 3 as
110+# published by the Free Software Foundation.
111+#
112+# charm-helpers is distributed in the hope that it will be useful,
113+# but WITHOUT ANY WARRANTY; without even the implied warranty of
114+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
115+# GNU Lesser General Public License for more details.
116+#
117+# You should have received a copy of the GNU Lesser General Public License
118+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
119
120=== added file 'hooks/charmhelpers/core/decorators.py'
121--- hooks/charmhelpers/core/decorators.py 1970-01-01 00:00:00 +0000
122+++ hooks/charmhelpers/core/decorators.py 2015-08-30 15:58:19 +0000
123@@ -0,0 +1,57 @@
124+# Copyright 2014-2015 Canonical Limited.
125+#
126+# This file is part of charm-helpers.
127+#
128+# charm-helpers is free software: you can redistribute it and/or modify
129+# it under the terms of the GNU Lesser General Public License version 3 as
130+# published by the Free Software Foundation.
131+#
132+# charm-helpers is distributed in the hope that it will be useful,
133+# but WITHOUT ANY WARRANTY; without even the implied warranty of
134+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
135+# GNU Lesser General Public License for more details.
136+#
137+# You should have received a copy of the GNU Lesser General Public License
138+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
139+
140+#
141+# Copyright 2014 Canonical Ltd.
142+#
143+# Authors:
144+# Edward Hope-Morley <opentastic@gmail.com>
145+#
146+
147+import time
148+
149+from charmhelpers.core.hookenv import (
150+ log,
151+ INFO,
152+)
153+
154+
155+def retry_on_exception(num_retries, base_delay=0, exc_type=Exception):
156+ """If the decorated function raises exception exc_type, allow num_retries
157+ retry attempts before raise the exception.
158+ """
159+ def _retry_on_exception_inner_1(f):
160+ def _retry_on_exception_inner_2(*args, **kwargs):
161+ retries = num_retries
162+ multiplier = 1
163+ while True:
164+ try:
165+ return f(*args, **kwargs)
166+ except exc_type:
167+ if not retries:
168+ raise
169+
170+ delay = base_delay * multiplier
171+ multiplier += 1
172+ log("Retrying '%s' %d more times (delay=%s)" %
173+ (f.__name__, retries, delay), level=INFO)
174+ retries -= 1
175+ if delay:
176+ time.sleep(delay)
177+
178+ return _retry_on_exception_inner_2
179+
180+ return _retry_on_exception_inner_1
181
182=== added file 'hooks/charmhelpers/core/files.py'
183--- hooks/charmhelpers/core/files.py 1970-01-01 00:00:00 +0000
184+++ hooks/charmhelpers/core/files.py 2015-08-30 15:58:19 +0000
185@@ -0,0 +1,45 @@
186+#!/usr/bin/env python
187+# -*- coding: utf-8 -*-
188+
189+# Copyright 2014-2015 Canonical Limited.
190+#
191+# This file is part of charm-helpers.
192+#
193+# charm-helpers is free software: you can redistribute it and/or modify
194+# it under the terms of the GNU Lesser General Public License version 3 as
195+# published by the Free Software Foundation.
196+#
197+# charm-helpers is distributed in the hope that it will be useful,
198+# but WITHOUT ANY WARRANTY; without even the implied warranty of
199+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
200+# GNU Lesser General Public License for more details.
201+#
202+# You should have received a copy of the GNU Lesser General Public License
203+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
204+
205+__author__ = 'Jorge Niedbalski <niedbalski@ubuntu.com>'
206+
207+import os
208+import subprocess
209+
210+
211+def sed(filename, before, after, flags='g'):
212+ """
213+ Search and replaces the given pattern on filename.
214+
215+ :param filename: relative or absolute file path.
216+ :param before: expression to be replaced (see 'man sed')
217+ :param after: expression to replace with (see 'man sed')
218+ :param flags: sed-compatible regex flags in example, to make
219+ the search and replace case insensitive, specify ``flags="i"``.
220+ The ``g`` flag is always specified regardless, so you do not
221+ need to remember to include it when overriding this parameter.
222+ :returns: If the sed command exit code was zero then return,
223+ otherwise raise CalledProcessError.
224+ """
225+ expression = r's/{0}/{1}/{2}'.format(before,
226+ after, flags)
227+
228+ return subprocess.check_call(["sed", "-i", "-r", "-e",
229+ expression,
230+ os.path.expanduser(filename)])
231
232=== added file 'hooks/charmhelpers/core/fstab.py'
233--- hooks/charmhelpers/core/fstab.py 1970-01-01 00:00:00 +0000
234+++ hooks/charmhelpers/core/fstab.py 2015-08-30 15:58:19 +0000
235@@ -0,0 +1,134 @@
236+#!/usr/bin/env python
237+# -*- coding: utf-8 -*-
238+
239+# Copyright 2014-2015 Canonical Limited.
240+#
241+# This file is part of charm-helpers.
242+#
243+# charm-helpers is free software: you can redistribute it and/or modify
244+# it under the terms of the GNU Lesser General Public License version 3 as
245+# published by the Free Software Foundation.
246+#
247+# charm-helpers is distributed in the hope that it will be useful,
248+# but WITHOUT ANY WARRANTY; without even the implied warranty of
249+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
250+# GNU Lesser General Public License for more details.
251+#
252+# You should have received a copy of the GNU Lesser General Public License
253+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
254+
255+import io
256+import os
257+
258+__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
259+
260+
261+class Fstab(io.FileIO):
262+ """This class extends file in order to implement a file reader/writer
263+ for file `/etc/fstab`
264+ """
265+
266+ class Entry(object):
267+ """Entry class represents a non-comment line on the `/etc/fstab` file
268+ """
269+ def __init__(self, device, mountpoint, filesystem,
270+ options, d=0, p=0):
271+ self.device = device
272+ self.mountpoint = mountpoint
273+ self.filesystem = filesystem
274+
275+ if not options:
276+ options = "defaults"
277+
278+ self.options = options
279+ self.d = int(d)
280+ self.p = int(p)
281+
282+ def __eq__(self, o):
283+ return str(self) == str(o)
284+
285+ def __str__(self):
286+ return "{} {} {} {} {} {}".format(self.device,
287+ self.mountpoint,
288+ self.filesystem,
289+ self.options,
290+ self.d,
291+ self.p)
292+
293+ DEFAULT_PATH = os.path.join(os.path.sep, 'etc', 'fstab')
294+
295+ def __init__(self, path=None):
296+ if path:
297+ self._path = path
298+ else:
299+ self._path = self.DEFAULT_PATH
300+ super(Fstab, self).__init__(self._path, 'rb+')
301+
302+ def _hydrate_entry(self, line):
303+ # NOTE: use split with no arguments to split on any
304+ # whitespace including tabs
305+ return Fstab.Entry(*filter(
306+ lambda x: x not in ('', None),
307+ line.strip("\n").split()))
308+
309+ @property
310+ def entries(self):
311+ self.seek(0)
312+ for line in self.readlines():
313+ line = line.decode('us-ascii')
314+ try:
315+ if line.strip() and not line.strip().startswith("#"):
316+ yield self._hydrate_entry(line)
317+ except ValueError:
318+ pass
319+
320+ def get_entry_by_attr(self, attr, value):
321+ for entry in self.entries:
322+ e_attr = getattr(entry, attr)
323+ if e_attr == value:
324+ return entry
325+ return None
326+
327+ def add_entry(self, entry):
328+ if self.get_entry_by_attr('device', entry.device):
329+ return False
330+
331+ self.write((str(entry) + '\n').encode('us-ascii'))
332+ self.truncate()
333+ return entry
334+
335+ def remove_entry(self, entry):
336+ self.seek(0)
337+
338+ lines = [l.decode('us-ascii') for l in self.readlines()]
339+
340+ found = False
341+ for index, line in enumerate(lines):
342+ if line.strip() and not line.strip().startswith("#"):
343+ if self._hydrate_entry(line) == entry:
344+ found = True
345+ break
346+
347+ if not found:
348+ return False
349+
350+ lines.remove(line)
351+
352+ self.seek(0)
353+ self.write(''.join(lines).encode('us-ascii'))
354+ self.truncate()
355+ return True
356+
357+ @classmethod
358+ def remove_by_mountpoint(cls, mountpoint, path=None):
359+ fstab = cls(path=path)
360+ entry = fstab.get_entry_by_attr('mountpoint', mountpoint)
361+ if entry:
362+ return fstab.remove_entry(entry)
363+ return False
364+
365+ @classmethod
366+ def add(cls, device, mountpoint, filesystem, options=None, path=None):
367+ return cls(path=path).add_entry(Fstab.Entry(device,
368+ mountpoint, filesystem,
369+ options=options))
370
371=== added file 'hooks/charmhelpers/core/hookenv.py'
372--- hooks/charmhelpers/core/hookenv.py 1970-01-01 00:00:00 +0000
373+++ hooks/charmhelpers/core/hookenv.py 2015-08-30 15:58:19 +0000
374@@ -0,0 +1,898 @@
375+# Copyright 2014-2015 Canonical Limited.
376+#
377+# This file is part of charm-helpers.
378+#
379+# charm-helpers is free software: you can redistribute it and/or modify
380+# it under the terms of the GNU Lesser General Public License version 3 as
381+# published by the Free Software Foundation.
382+#
383+# charm-helpers is distributed in the hope that it will be useful,
384+# but WITHOUT ANY WARRANTY; without even the implied warranty of
385+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
386+# GNU Lesser General Public License for more details.
387+#
388+# You should have received a copy of the GNU Lesser General Public License
389+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
390+
391+"Interactions with the Juju environment"
392+# Copyright 2013 Canonical Ltd.
393+#
394+# Authors:
395+# Charm Helpers Developers <juju@lists.ubuntu.com>
396+
397+from __future__ import print_function
398+import copy
399+from distutils.version import LooseVersion
400+from functools import wraps
401+import glob
402+import os
403+import json
404+import yaml
405+import subprocess
406+import sys
407+import errno
408+import tempfile
409+from subprocess import CalledProcessError
410+
411+import six
412+if not six.PY3:
413+ from UserDict import UserDict
414+else:
415+ from collections import UserDict
416+
417+CRITICAL = "CRITICAL"
418+ERROR = "ERROR"
419+WARNING = "WARNING"
420+INFO = "INFO"
421+DEBUG = "DEBUG"
422+MARKER = object()
423+
424+cache = {}
425+
426+
427+def cached(func):
428+ """Cache return values for multiple executions of func + args
429+
430+ For example::
431+
432+ @cached
433+ def unit_get(attribute):
434+ pass
435+
436+ unit_get('test')
437+
438+ will cache the result of unit_get + 'test' for future calls.
439+ """
440+ @wraps(func)
441+ def wrapper(*args, **kwargs):
442+ global cache
443+ key = str((func, args, kwargs))
444+ try:
445+ return cache[key]
446+ except KeyError:
447+ pass # Drop out of the exception handler scope.
448+ res = func(*args, **kwargs)
449+ cache[key] = res
450+ return res
451+ wrapper._wrapped = func
452+ return wrapper
453+
454+
455+def flush(key):
456+ """Flushes any entries from function cache where the
457+ key is found in the function+args """
458+ flush_list = []
459+ for item in cache:
460+ if key in item:
461+ flush_list.append(item)
462+ for item in flush_list:
463+ del cache[item]
464+
465+
466+def log(message, level=None):
467+ """Write a message to the juju log"""
468+ command = ['juju-log']
469+ if level:
470+ command += ['-l', level]
471+ if not isinstance(message, six.string_types):
472+ message = repr(message)
473+ command += [message]
474+ # Missing juju-log should not cause failures in unit tests
475+ # Send log output to stderr
476+ try:
477+ subprocess.call(command)
478+ except OSError as e:
479+ if e.errno == errno.ENOENT:
480+ if level:
481+ message = "{}: {}".format(level, message)
482+ message = "juju-log: {}".format(message)
483+ print(message, file=sys.stderr)
484+ else:
485+ raise
486+
487+
488+class Serializable(UserDict):
489+ """Wrapper, an object that can be serialized to yaml or json"""
490+
491+ def __init__(self, obj):
492+ # wrap the object
493+ UserDict.__init__(self)
494+ self.data = obj
495+
496+ def __getattr__(self, attr):
497+ # See if this object has attribute.
498+ if attr in ("json", "yaml", "data"):
499+ return self.__dict__[attr]
500+ # Check for attribute in wrapped object.
501+ got = getattr(self.data, attr, MARKER)
502+ if got is not MARKER:
503+ return got
504+ # Proxy to the wrapped object via dict interface.
505+ try:
506+ return self.data[attr]
507+ except KeyError:
508+ raise AttributeError(attr)
509+
510+ def __getstate__(self):
511+ # Pickle as a standard dictionary.
512+ return self.data
513+
514+ def __setstate__(self, state):
515+ # Unpickle into our wrapper.
516+ self.data = state
517+
518+ def json(self):
519+ """Serialize the object to json"""
520+ return json.dumps(self.data)
521+
522+ def yaml(self):
523+ """Serialize the object to yaml"""
524+ return yaml.dump(self.data)
525+
526+
527+def execution_environment():
528+ """A convenient bundling of the current execution context"""
529+ context = {}
530+ context['conf'] = config()
531+ if relation_id():
532+ context['reltype'] = relation_type()
533+ context['relid'] = relation_id()
534+ context['rel'] = relation_get()
535+ context['unit'] = local_unit()
536+ context['rels'] = relations()
537+ context['env'] = os.environ
538+ return context
539+
540+
541+def in_relation_hook():
542+ """Determine whether we're running in a relation hook"""
543+ return 'JUJU_RELATION' in os.environ
544+
545+
546+def relation_type():
547+ """The scope for the current relation hook"""
548+ return os.environ.get('JUJU_RELATION', None)
549+
550+
551+@cached
552+def relation_id(relation_name=None, service_or_unit=None):
553+ """The relation ID for the current or a specified relation"""
554+ if not relation_name and not service_or_unit:
555+ return os.environ.get('JUJU_RELATION_ID', None)
556+ elif relation_name and service_or_unit:
557+ service_name = service_or_unit.split('/')[0]
558+ for relid in relation_ids(relation_name):
559+ remote_service = remote_service_name(relid)
560+ if remote_service == service_name:
561+ return relid
562+ else:
563+ raise ValueError('Must specify neither or both of relation_name and service_or_unit')
564+
565+
566+def local_unit():
567+ """Local unit ID"""
568+ return os.environ['JUJU_UNIT_NAME']
569+
570+
571+def remote_unit():
572+ """The remote unit for the current relation hook"""
573+ return os.environ.get('JUJU_REMOTE_UNIT', None)
574+
575+
576+def service_name():
577+ """The name service group this unit belongs to"""
578+ return local_unit().split('/')[0]
579+
580+
581+@cached
582+def remote_service_name(relid=None):
583+ """The remote service name for a given relation-id (or the current relation)"""
584+ if relid is None:
585+ unit = remote_unit()
586+ else:
587+ units = related_units(relid)
588+ unit = units[0] if units else None
589+ return unit.split('/')[0] if unit else None
590+
591+
592+def hook_name():
593+ """The name of the currently executing hook"""
594+ return os.environ.get('JUJU_HOOK_NAME', os.path.basename(sys.argv[0]))
595+
596+
597+class Config(dict):
598+ """A dictionary representation of the charm's config.yaml, with some
599+ extra features:
600+
601+ - See which values in the dictionary have changed since the previous hook.
602+ - For values that have changed, see what the previous value was.
603+ - Store arbitrary data for use in a later hook.
604+
605+ NOTE: Do not instantiate this object directly - instead call
606+ ``hookenv.config()``, which will return an instance of :class:`Config`.
607+
608+ Example usage::
609+
610+ >>> # inside a hook
611+ >>> from charmhelpers.core import hookenv
612+ >>> config = hookenv.config()
613+ >>> config['foo']
614+ 'bar'
615+ >>> # store a new key/value for later use
616+ >>> config['mykey'] = 'myval'
617+
618+
619+ >>> # user runs `juju set mycharm foo=baz`
620+ >>> # now we're inside subsequent config-changed hook
621+ >>> config = hookenv.config()
622+ >>> config['foo']
623+ 'baz'
624+ >>> # test to see if this val has changed since last hook
625+ >>> config.changed('foo')
626+ True
627+ >>> # what was the previous value?
628+ >>> config.previous('foo')
629+ 'bar'
630+ >>> # keys/values that we add are preserved across hooks
631+ >>> config['mykey']
632+ 'myval'
633+
634+ """
635+ CONFIG_FILE_NAME = '.juju-persistent-config'
636+
637+ def __init__(self, *args, **kw):
638+ super(Config, self).__init__(*args, **kw)
639+ self.implicit_save = True
640+ self._prev_dict = None
641+ self.path = os.path.join(charm_dir(), Config.CONFIG_FILE_NAME)
642+ if os.path.exists(self.path):
643+ self.load_previous()
644+ atexit(self._implicit_save)
645+
646+ def load_previous(self, path=None):
647+ """Load previous copy of config from disk.
648+
649+ In normal usage you don't need to call this method directly - it
650+ is called automatically at object initialization.
651+
652+ :param path:
653+
654+ File path from which to load the previous config. If `None`,
655+ config is loaded from the default location. If `path` is
656+ specified, subsequent `save()` calls will write to the same
657+ path.
658+
659+ """
660+ self.path = path or self.path
661+ with open(self.path) as f:
662+ self._prev_dict = json.load(f)
663+ for k, v in copy.deepcopy(self._prev_dict).items():
664+ if k not in self:
665+ self[k] = v
666+
667+ def changed(self, key):
668+ """Return True if the current value for this key is different from
669+ the previous value.
670+
671+ """
672+ if self._prev_dict is None:
673+ return True
674+ return self.previous(key) != self.get(key)
675+
676+ def previous(self, key):
677+ """Return previous value for this key, or None if there
678+ is no previous value.
679+
680+ """
681+ if self._prev_dict:
682+ return self._prev_dict.get(key)
683+ return None
684+
685+ def save(self):
686+ """Save this config to disk.
687+
688+ If the charm is using the :mod:`Services Framework <services.base>`
689+ or :meth:'@hook <Hooks.hook>' decorator, this
690+ is called automatically at the end of successful hook execution.
691+ Otherwise, it should be called directly by user code.
692+
693+ To disable automatic saves, set ``implicit_save=False`` on this
694+ instance.
695+
696+ """
697+ with open(self.path, 'w') as f:
698+ json.dump(self, f)
699+
700+ def _implicit_save(self):
701+ if self.implicit_save:
702+ self.save()
703+
704+
705+@cached
706+def config(scope=None):
707+ """Juju charm configuration"""
708+ config_cmd_line = ['config-get']
709+ if scope is not None:
710+ config_cmd_line.append(scope)
711+ config_cmd_line.append('--format=json')
712+ try:
713+ config_data = json.loads(
714+ subprocess.check_output(config_cmd_line).decode('UTF-8'))
715+ if scope is not None:
716+ return config_data
717+ return Config(config_data)
718+ except ValueError:
719+ return None
720+
721+
722+@cached
723+def relation_get(attribute=None, unit=None, rid=None):
724+ """Get relation information"""
725+ _args = ['relation-get', '--format=json']
726+ if rid:
727+ _args.append('-r')
728+ _args.append(rid)
729+ _args.append(attribute or '-')
730+ if unit:
731+ _args.append(unit)
732+ try:
733+ return json.loads(subprocess.check_output(_args).decode('UTF-8'))
734+ except ValueError:
735+ return None
736+ except CalledProcessError as e:
737+ if e.returncode == 2:
738+ return None
739+ raise
740+
741+
742+def relation_set(relation_id=None, relation_settings=None, **kwargs):
743+ """Set relation information for the current unit"""
744+ relation_settings = relation_settings if relation_settings else {}
745+ relation_cmd_line = ['relation-set']
746+ accepts_file = "--file" in subprocess.check_output(
747+ relation_cmd_line + ["--help"], universal_newlines=True)
748+ if relation_id is not None:
749+ relation_cmd_line.extend(('-r', relation_id))
750+ settings = relation_settings.copy()
751+ settings.update(kwargs)
752+ for key, value in settings.items():
753+ # Force value to be a string: it always should, but some call
754+ # sites pass in things like dicts or numbers.
755+ if value is not None:
756+ settings[key] = "{}".format(value)
757+ if accepts_file:
758+ # --file was introduced in Juju 1.23.2. Use it by default if
759+ # available, since otherwise we'll break if the relation data is
760+ # too big. Ideally we should tell relation-set to read the data from
761+ # stdin, but that feature is broken in 1.23.2: Bug #1454678.
762+ with tempfile.NamedTemporaryFile(delete=False) as settings_file:
763+ settings_file.write(yaml.safe_dump(settings).encode("utf-8"))
764+ subprocess.check_call(
765+ relation_cmd_line + ["--file", settings_file.name])
766+ os.remove(settings_file.name)
767+ else:
768+ for key, value in settings.items():
769+ if value is None:
770+ relation_cmd_line.append('{}='.format(key))
771+ else:
772+ relation_cmd_line.append('{}={}'.format(key, value))
773+ subprocess.check_call(relation_cmd_line)
774+ # Flush cache of any relation-gets for local unit
775+ flush(local_unit())
776+
777+
778+def relation_clear(r_id=None):
779+ ''' Clears any relation data already set on relation r_id '''
780+ settings = relation_get(rid=r_id,
781+ unit=local_unit())
782+ for setting in settings:
783+ if setting not in ['public-address', 'private-address']:
784+ settings[setting] = None
785+ relation_set(relation_id=r_id,
786+ **settings)
787+
788+
789+@cached
790+def relation_ids(reltype=None):
791+ """A list of relation_ids"""
792+ reltype = reltype or relation_type()
793+ relid_cmd_line = ['relation-ids', '--format=json']
794+ if reltype is not None:
795+ relid_cmd_line.append(reltype)
796+ return json.loads(
797+ subprocess.check_output(relid_cmd_line).decode('UTF-8')) or []
798+ return []
799+
800+
801+@cached
802+def related_units(relid=None):
803+ """A list of related units"""
804+ relid = relid or relation_id()
805+ units_cmd_line = ['relation-list', '--format=json']
806+ if relid is not None:
807+ units_cmd_line.extend(('-r', relid))
808+ return json.loads(
809+ subprocess.check_output(units_cmd_line).decode('UTF-8')) or []
810+
811+
812+@cached
813+def relation_for_unit(unit=None, rid=None):
814+ """Get the json represenation of a unit's relation"""
815+ unit = unit or remote_unit()
816+ relation = relation_get(unit=unit, rid=rid)
817+ for key in relation:
818+ if key.endswith('-list'):
819+ relation[key] = relation[key].split()
820+ relation['__unit__'] = unit
821+ return relation
822+
823+
824+@cached
825+def relations_for_id(relid=None):
826+ """Get relations of a specific relation ID"""
827+ relation_data = []
828+ relid = relid or relation_ids()
829+ for unit in related_units(relid):
830+ unit_data = relation_for_unit(unit, relid)
831+ unit_data['__relid__'] = relid
832+ relation_data.append(unit_data)
833+ return relation_data
834+
835+
836+@cached
837+def relations_of_type(reltype=None):
838+ """Get relations of a specific type"""
839+ relation_data = []
840+ reltype = reltype or relation_type()
841+ for relid in relation_ids(reltype):
842+ for relation in relations_for_id(relid):
843+ relation['__relid__'] = relid
844+ relation_data.append(relation)
845+ return relation_data
846+
847+
848+@cached
849+def metadata():
850+ """Get the current charm metadata.yaml contents as a python object"""
851+ with open(os.path.join(charm_dir(), 'metadata.yaml')) as md:
852+ return yaml.safe_load(md)
853+
854+
855+@cached
856+def relation_types():
857+ """Get a list of relation types supported by this charm"""
858+ rel_types = []
859+ md = metadata()
860+ for key in ('provides', 'requires', 'peers'):
861+ section = md.get(key)
862+ if section:
863+ rel_types.extend(section.keys())
864+ return rel_types
865+
866+
867+@cached
868+def relation_to_interface(relation_name):
869+ """
870+ Given the name of a relation, return the interface that relation uses.
871+
872+ :returns: The interface name, or ``None``.
873+ """
874+ return relation_to_role_and_interface(relation_name)[1]
875+
876+
877+@cached
878+def relation_to_role_and_interface(relation_name):
879+ """
880+ Given the name of a relation, return the role and the name of the interface
881+ that relation uses (where role is one of ``provides``, ``requires``, or ``peer``).
882+
883+ :returns: A tuple containing ``(role, interface)``, or ``(None, None)``.
884+ """
885+ _metadata = metadata()
886+ for role in ('provides', 'requires', 'peer'):
887+ interface = _metadata.get(role, {}).get(relation_name, {}).get('interface')
888+ if interface:
889+ return role, interface
890+ return None, None
891+
892+
893+@cached
894+def role_and_interface_to_relations(role, interface_name):
895+ """
896+ Given a role and interface name, return a list of relation names for the
897+ current charm that use that interface under that role (where role is one
898+ of ``provides``, ``requires``, or ``peer``).
899+
900+ :returns: A list of relation names.
901+ """
902+ _metadata = metadata()
903+ results = []
904+ for relation_name, relation in _metadata.get(role, {}).items():
905+ if relation['interface'] == interface_name:
906+ results.append(relation_name)
907+ return results
908+
909+
910+@cached
911+def interface_to_relations(interface_name):
912+ """
913+ Given an interface, return a list of relation names for the current
914+ charm that use that interface.
915+
916+ :returns: A list of relation names.
917+ """
918+ results = []
919+ for role in ('provides', 'requires', 'peer'):
920+ results.extend(role_and_interface_to_relations(role, interface_name))
921+ return results
922+
923+
924+@cached
925+def charm_name():
926+ """Get the name of the current charm as is specified on metadata.yaml"""
927+ return metadata().get('name')
928+
929+
930+@cached
931+def relations():
932+ """Get a nested dictionary of relation data for all related units"""
933+ rels = {}
934+ for reltype in relation_types():
935+ relids = {}
936+ for relid in relation_ids(reltype):
937+ units = {local_unit(): relation_get(unit=local_unit(), rid=relid)}
938+ for unit in related_units(relid):
939+ reldata = relation_get(unit=unit, rid=relid)
940+ units[unit] = reldata
941+ relids[relid] = units
942+ rels[reltype] = relids
943+ return rels
944+
945+
946+@cached
947+def is_relation_made(relation, keys='private-address'):
948+ '''
949+ Determine whether a relation is established by checking for
950+ presence of key(s). If a list of keys is provided, they
951+ must all be present for the relation to be identified as made
952+ '''
953+ if isinstance(keys, str):
954+ keys = [keys]
955+ for r_id in relation_ids(relation):
956+ for unit in related_units(r_id):
957+ context = {}
958+ for k in keys:
959+ context[k] = relation_get(k, rid=r_id,
960+ unit=unit)
961+ if None not in context.values():
962+ return True
963+ return False
964+
965+
966+def open_port(port, protocol="TCP"):
967+ """Open a service network port"""
968+ _args = ['open-port']
969+ _args.append('{}/{}'.format(port, protocol))
970+ subprocess.check_call(_args)
971+
972+
973+def close_port(port, protocol="TCP"):
974+ """Close a service network port"""
975+ _args = ['close-port']
976+ _args.append('{}/{}'.format(port, protocol))
977+ subprocess.check_call(_args)
978+
979+
980+@cached
981+def unit_get(attribute):
982+ """Get the unit ID for the remote unit"""
983+ _args = ['unit-get', '--format=json', attribute]
984+ try:
985+ return json.loads(subprocess.check_output(_args).decode('UTF-8'))
986+ except ValueError:
987+ return None
988+
989+
990+def unit_public_ip():
991+ """Get this unit's public IP address"""
992+ return unit_get('public-address')
993+
994+
995+def unit_private_ip():
996+ """Get this unit's private IP address"""
997+ return unit_get('private-address')
998+
999+
1000+class UnregisteredHookError(Exception):
1001+ """Raised when an undefined hook is called"""
1002+ pass
1003+
1004+
1005+class Hooks(object):
1006+ """A convenient handler for hook functions.
1007+
1008+ Example::
1009+
1010+ hooks = Hooks()
1011+
1012+ # register a hook, taking its name from the function name
1013+ @hooks.hook()
1014+ def install():
1015+ pass # your code here
1016+
1017+ # register a hook, providing a custom hook name
1018+ @hooks.hook("config-changed")
1019+ def config_changed():
1020+ pass # your code here
1021+
1022+ if __name__ == "__main__":
1023+ # execute a hook based on the name the program is called by
1024+ hooks.execute(sys.argv)
1025+ """
1026+
1027+ def __init__(self, config_save=None):
1028+ super(Hooks, self).__init__()
1029+ self._hooks = {}
1030+
1031+ # For unknown reasons, we allow the Hooks constructor to override
1032+ # config().implicit_save.
1033+ if config_save is not None:
1034+ config().implicit_save = config_save
1035+
1036+ def register(self, name, function):
1037+ """Register a hook"""
1038+ self._hooks[name] = function
1039+
1040+ def execute(self, args):
1041+ """Execute a registered hook based on args[0]"""
1042+ _run_atstart()
1043+ hook_name = os.path.basename(args[0])
1044+ if hook_name in self._hooks:
1045+ try:
1046+ self._hooks[hook_name]()
1047+ except SystemExit as x:
1048+ if x.code is None or x.code == 0:
1049+ _run_atexit()
1050+ raise
1051+ _run_atexit()
1052+ else:
1053+ raise UnregisteredHookError(hook_name)
1054+
1055+ def hook(self, *hook_names):
1056+ """Decorator, registering them as hooks"""
1057+ def wrapper(decorated):
1058+ for hook_name in hook_names:
1059+ self.register(hook_name, decorated)
1060+ else:
1061+ self.register(decorated.__name__, decorated)
1062+ if '_' in decorated.__name__:
1063+ self.register(
1064+ decorated.__name__.replace('_', '-'), decorated)
1065+ return decorated
1066+ return wrapper
1067+
1068+
1069+def charm_dir():
1070+ """Return the root directory of the current charm"""
1071+ return os.environ.get('CHARM_DIR')
1072+
1073+
1074+@cached
1075+def action_get(key=None):
1076+ """Gets the value of an action parameter, or all key/value param pairs"""
1077+ cmd = ['action-get']
1078+ if key is not None:
1079+ cmd.append(key)
1080+ cmd.append('--format=json')
1081+ action_data = json.loads(subprocess.check_output(cmd).decode('UTF-8'))
1082+ return action_data
1083+
1084+
1085+def action_set(values):
1086+ """Sets the values to be returned after the action finishes"""
1087+ cmd = ['action-set']
1088+ for k, v in list(values.items()):
1089+ cmd.append('{}={}'.format(k, v))
1090+ subprocess.check_call(cmd)
1091+
1092+
1093+def action_fail(message):
1094+ """Sets the action status to failed and sets the error message.
1095+
1096+ The results set by action_set are preserved."""
1097+ subprocess.check_call(['action-fail', message])
1098+
1099+
1100+def action_name():
1101+ """Get the name of the currently executing action."""
1102+ return os.environ.get('JUJU_ACTION_NAME')
1103+
1104+
1105+def action_uuid():
1106+ """Get the UUID of the currently executing action."""
1107+ return os.environ.get('JUJU_ACTION_UUID')
1108+
1109+
1110+def action_tag():
1111+ """Get the tag for the currently executing action."""
1112+ return os.environ.get('JUJU_ACTION_TAG')
1113+
1114+
1115+def status_set(workload_state, message):
1116+ """Set the workload state with a message
1117+
1118+ Use status-set to set the workload state with a message which is visible
1119+ to the user via juju status. If the status-set command is not found then
1120+ assume this is juju < 1.23 and juju-log the message unstead.
1121+
1122+ workload_state -- valid juju workload state.
1123+ message -- status update message
1124+ """
1125+ valid_states = ['maintenance', 'blocked', 'waiting', 'active']
1126+ if workload_state not in valid_states:
1127+ raise ValueError(
1128+ '{!r} is not a valid workload state'.format(workload_state)
1129+ )
1130+ cmd = ['status-set', workload_state, message]
1131+ try:
1132+ ret = subprocess.call(cmd)
1133+ if ret == 0:
1134+ return
1135+ except OSError as e:
1136+ if e.errno != errno.ENOENT:
1137+ raise
1138+ log_message = 'status-set failed: {} {}'.format(workload_state,
1139+ message)
1140+ log(log_message, level='INFO')
1141+
1142+
1143+def status_get():
1144+ """Retrieve the previously set juju workload state and message
1145+
1146+ If the status-get command is not found then assume this is juju < 1.23 and
1147+ return 'unknown', ""
1148+
1149+ """
1150+ cmd = ['status-get', "--format=json", "--include-data"]
1151+ try:
1152+ raw_status = subprocess.check_output(cmd)
1153+ except OSError as e:
1154+ if e.errno == errno.ENOENT:
1155+ return ('unknown', "")
1156+ else:
1157+ raise
1158+ else:
1159+ status = json.loads(raw_status.decode("UTF-8"))
1160+ return (status["status"], status["message"])
1161+
1162+
1163+def translate_exc(from_exc, to_exc):
1164+ def inner_translate_exc1(f):
1165+ def inner_translate_exc2(*args, **kwargs):
1166+ try:
1167+ return f(*args, **kwargs)
1168+ except from_exc:
1169+ raise to_exc
1170+
1171+ return inner_translate_exc2
1172+
1173+ return inner_translate_exc1
1174+
1175+
1176+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
1177+def is_leader():
1178+ """Does the current unit hold the juju leadership
1179+
1180+ Uses juju to determine whether the current unit is the leader of its peers
1181+ """
1182+ cmd = ['is-leader', '--format=json']
1183+ return json.loads(subprocess.check_output(cmd).decode('UTF-8'))
1184+
1185+
1186+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
1187+def leader_get(attribute=None):
1188+ """Juju leader get value(s)"""
1189+ cmd = ['leader-get', '--format=json'] + [attribute or '-']
1190+ return json.loads(subprocess.check_output(cmd).decode('UTF-8'))
1191+
1192+
1193+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
1194+def leader_set(settings=None, **kwargs):
1195+ """Juju leader set value(s)"""
1196+ # Don't log secrets.
1197+ # log("Juju leader-set '%s'" % (settings), level=DEBUG)
1198+ cmd = ['leader-set']
1199+ settings = settings or {}
1200+ settings.update(kwargs)
1201+ for k, v in settings.items():
1202+ if v is None:
1203+ cmd.append('{}='.format(k))
1204+ else:
1205+ cmd.append('{}={}'.format(k, v))
1206+ subprocess.check_call(cmd)
1207+
1208+
1209+@cached
1210+def juju_version():
1211+ """Full version string (eg. '1.23.3.1-trusty-amd64')"""
1212+ # Per https://bugs.launchpad.net/juju-core/+bug/1455368/comments/1
1213+ jujud = glob.glob('/var/lib/juju/tools/machine-*/jujud')[0]
1214+ return subprocess.check_output([jujud, 'version'],
1215+ universal_newlines=True).strip()
1216+
1217+
1218+@cached
1219+def has_juju_version(minimum_version):
1220+ """Return True if the Juju version is at least the provided version"""
1221+ return LooseVersion(juju_version()) >= LooseVersion(minimum_version)
1222+
1223+
1224+_atexit = []
1225+_atstart = []
1226+
1227+
1228+def atstart(callback, *args, **kwargs):
1229+ '''Schedule a callback to run before the main hook.
1230+
1231+ Callbacks are run in the order they were added.
1232+
1233+ This is useful for modules and classes to perform initialization
1234+ and inject behavior. In particular:
1235+
1236+ - Run common code before all of your hooks, such as logging
1237+ the hook name or interesting relation data.
1238+ - Defer object or module initialization that requires a hook
1239+ context until we know there actually is a hook context,
1240+ making testing easier.
1241+ - Rather than requiring charm authors to include boilerplate to
1242+ invoke your helper's behavior, have it run automatically if
1243+ your object is instantiated or module imported.
1244+
1245+ This is not at all useful after your hook framework as been launched.
1246+ '''
1247+ global _atstart
1248+ _atstart.append((callback, args, kwargs))
1249+
1250+
1251+def atexit(callback, *args, **kwargs):
1252+ '''Schedule a callback to run on successful hook completion.
1253+
1254+ Callbacks are run in the reverse order that they were added.'''
1255+ _atexit.append((callback, args, kwargs))
1256+
1257+
1258+def _run_atstart():
1259+ '''Hook frameworks must invoke this before running the main hook body.'''
1260+ global _atstart
1261+ for callback, args, kwargs in _atstart:
1262+ callback(*args, **kwargs)
1263+ del _atstart[:]
1264+
1265+
1266+def _run_atexit():
1267+ '''Hook frameworks must invoke this after the main hook body has
1268+ successfully completed. Do not invoke it if the hook fails.'''
1269+ global _atexit
1270+ for callback, args, kwargs in reversed(_atexit):
1271+ callback(*args, **kwargs)
1272+ del _atexit[:]
1273
1274=== added file 'hooks/charmhelpers/core/host.py'
1275--- hooks/charmhelpers/core/host.py 1970-01-01 00:00:00 +0000
1276+++ hooks/charmhelpers/core/host.py 2015-08-30 15:58:19 +0000
1277@@ -0,0 +1,570 @@
1278+# Copyright 2014-2015 Canonical Limited.
1279+#
1280+# This file is part of charm-helpers.
1281+#
1282+# charm-helpers is free software: you can redistribute it and/or modify
1283+# it under the terms of the GNU Lesser General Public License version 3 as
1284+# published by the Free Software Foundation.
1285+#
1286+# charm-helpers is distributed in the hope that it will be useful,
1287+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1288+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1289+# GNU Lesser General Public License for more details.
1290+#
1291+# You should have received a copy of the GNU Lesser General Public License
1292+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1293+
1294+"""Tools for working with the host system"""
1295+# Copyright 2012 Canonical Ltd.
1296+#
1297+# Authors:
1298+# Nick Moffitt <nick.moffitt@canonical.com>
1299+# Matthew Wedgwood <matthew.wedgwood@canonical.com>
1300+
1301+import os
1302+import re
1303+import pwd
1304+import glob
1305+import grp
1306+import random
1307+import string
1308+import subprocess
1309+import hashlib
1310+from contextlib import contextmanager
1311+from collections import OrderedDict
1312+
1313+import six
1314+
1315+from .hookenv import log
1316+from .fstab import Fstab
1317+
1318+
1319+def service_start(service_name):
1320+ """Start a system service"""
1321+ return service('start', service_name)
1322+
1323+
1324+def service_stop(service_name):
1325+ """Stop a system service"""
1326+ return service('stop', service_name)
1327+
1328+
1329+def service_restart(service_name):
1330+ """Restart a system service"""
1331+ return service('restart', service_name)
1332+
1333+
1334+def service_reload(service_name, restart_on_failure=False):
1335+ """Reload a system service, optionally falling back to restart if
1336+ reload fails"""
1337+ service_result = service('reload', service_name)
1338+ if not service_result and restart_on_failure:
1339+ service_result = service('restart', service_name)
1340+ return service_result
1341+
1342+
1343+def service_pause(service_name, init_dir=None):
1344+ """Pause a system service.
1345+
1346+ Stop it, and prevent it from starting again at boot."""
1347+ if init_dir is None:
1348+ init_dir = "/etc/init"
1349+ stopped = service_stop(service_name)
1350+ # XXX: Support systemd too
1351+ override_path = os.path.join(
1352+ init_dir, '{}.override'.format(service_name))
1353+ with open(override_path, 'w') as fh:
1354+ fh.write("manual\n")
1355+ return stopped
1356+
1357+
1358+def service_resume(service_name, init_dir=None):
1359+ """Resume a system service.
1360+
1361+ Reenable starting again at boot. Start the service"""
1362+ # XXX: Support systemd too
1363+ if init_dir is None:
1364+ init_dir = "/etc/init"
1365+ override_path = os.path.join(
1366+ init_dir, '{}.override'.format(service_name))
1367+ if os.path.exists(override_path):
1368+ os.unlink(override_path)
1369+ started = service_start(service_name)
1370+ return started
1371+
1372+
1373+def service(action, service_name):
1374+ """Control a system service"""
1375+ cmd = ['service', service_name, action]
1376+ return subprocess.call(cmd) == 0
1377+
1378+
1379+def service_running(service):
1380+ """Determine whether a system service is running"""
1381+ try:
1382+ output = subprocess.check_output(
1383+ ['service', service, 'status'],
1384+ stderr=subprocess.STDOUT).decode('UTF-8')
1385+ except subprocess.CalledProcessError:
1386+ return False
1387+ else:
1388+ if ("start/running" in output or "is running" in output):
1389+ return True
1390+ else:
1391+ return False
1392+
1393+
1394+def service_available(service_name):
1395+ """Determine whether a system service is available"""
1396+ try:
1397+ subprocess.check_output(
1398+ ['service', service_name, 'status'],
1399+ stderr=subprocess.STDOUT).decode('UTF-8')
1400+ except subprocess.CalledProcessError as e:
1401+ return b'unrecognized service' not in e.output
1402+ else:
1403+ return True
1404+
1405+
1406+def adduser(username, password=None, shell='/bin/bash', system_user=False):
1407+ """Add a user to the system"""
1408+ try:
1409+ user_info = pwd.getpwnam(username)
1410+ log('user {0} already exists!'.format(username))
1411+ except KeyError:
1412+ log('creating user {0}'.format(username))
1413+ cmd = ['useradd']
1414+ if system_user or password is None:
1415+ cmd.append('--system')
1416+ else:
1417+ cmd.extend([
1418+ '--create-home',
1419+ '--shell', shell,
1420+ '--password', password,
1421+ ])
1422+ cmd.append(username)
1423+ subprocess.check_call(cmd)
1424+ user_info = pwd.getpwnam(username)
1425+ return user_info
1426+
1427+
1428+def user_exists(username):
1429+ """Check if a user exists"""
1430+ try:
1431+ pwd.getpwnam(username)
1432+ user_exists = True
1433+ except KeyError:
1434+ user_exists = False
1435+ return user_exists
1436+
1437+
1438+def add_group(group_name, system_group=False):
1439+ """Add a group to the system"""
1440+ try:
1441+ group_info = grp.getgrnam(group_name)
1442+ log('group {0} already exists!'.format(group_name))
1443+ except KeyError:
1444+ log('creating group {0}'.format(group_name))
1445+ cmd = ['addgroup']
1446+ if system_group:
1447+ cmd.append('--system')
1448+ else:
1449+ cmd.extend([
1450+ '--group',
1451+ ])
1452+ cmd.append(group_name)
1453+ subprocess.check_call(cmd)
1454+ group_info = grp.getgrnam(group_name)
1455+ return group_info
1456+
1457+
1458+def add_user_to_group(username, group):
1459+ """Add a user to a group"""
1460+ cmd = ['gpasswd', '-a', username, group]
1461+ log("Adding user {} to group {}".format(username, group))
1462+ subprocess.check_call(cmd)
1463+
1464+
1465+def rsync(from_path, to_path, flags='-r', options=None):
1466+ """Replicate the contents of a path"""
1467+ options = options or ['--delete', '--executability']
1468+ cmd = ['/usr/bin/rsync', flags]
1469+ cmd.extend(options)
1470+ cmd.append(from_path)
1471+ cmd.append(to_path)
1472+ log(" ".join(cmd))
1473+ return subprocess.check_output(cmd).decode('UTF-8').strip()
1474+
1475+
1476+def symlink(source, destination):
1477+ """Create a symbolic link"""
1478+ log("Symlinking {} as {}".format(source, destination))
1479+ cmd = [
1480+ 'ln',
1481+ '-sf',
1482+ source,
1483+ destination,
1484+ ]
1485+ subprocess.check_call(cmd)
1486+
1487+
1488+def mkdir(path, owner='root', group='root', perms=0o555, force=False):
1489+ """Create a directory"""
1490+ log("Making dir {} {}:{} {:o}".format(path, owner, group,
1491+ perms))
1492+ uid = pwd.getpwnam(owner).pw_uid
1493+ gid = grp.getgrnam(group).gr_gid
1494+ realpath = os.path.abspath(path)
1495+ path_exists = os.path.exists(realpath)
1496+ if path_exists and force:
1497+ if not os.path.isdir(realpath):
1498+ log("Removing non-directory file {} prior to mkdir()".format(path))
1499+ os.unlink(realpath)
1500+ os.makedirs(realpath, perms)
1501+ elif not path_exists:
1502+ os.makedirs(realpath, perms)
1503+ os.chown(realpath, uid, gid)
1504+ os.chmod(realpath, perms)
1505+
1506+
1507+def write_file(path, content, owner='root', group='root', perms=0o444):
1508+ """Create or overwrite a file with the contents of a byte string."""
1509+ log("Writing file {} {}:{} {:o}".format(path, owner, group, perms))
1510+ uid = pwd.getpwnam(owner).pw_uid
1511+ gid = grp.getgrnam(group).gr_gid
1512+ with open(path, 'wb') as target:
1513+ os.fchown(target.fileno(), uid, gid)
1514+ os.fchmod(target.fileno(), perms)
1515+ target.write(content)
1516+
1517+
1518+def fstab_remove(mp):
1519+ """Remove the given mountpoint entry from /etc/fstab
1520+ """
1521+ return Fstab.remove_by_mountpoint(mp)
1522+
1523+
1524+def fstab_add(dev, mp, fs, options=None):
1525+ """Adds the given device entry to the /etc/fstab file
1526+ """
1527+ return Fstab.add(dev, mp, fs, options=options)
1528+
1529+
1530+def mount(device, mountpoint, options=None, persist=False, filesystem="ext3"):
1531+ """Mount a filesystem at a particular mountpoint"""
1532+ cmd_args = ['mount']
1533+ if options is not None:
1534+ cmd_args.extend(['-o', options])
1535+ cmd_args.extend([device, mountpoint])
1536+ try:
1537+ subprocess.check_output(cmd_args)
1538+ except subprocess.CalledProcessError as e:
1539+ log('Error mounting {} at {}\n{}'.format(device, mountpoint, e.output))
1540+ return False
1541+
1542+ if persist:
1543+ return fstab_add(device, mountpoint, filesystem, options=options)
1544+ return True
1545+
1546+
1547+def umount(mountpoint, persist=False):
1548+ """Unmount a filesystem"""
1549+ cmd_args = ['umount', mountpoint]
1550+ try:
1551+ subprocess.check_output(cmd_args)
1552+ except subprocess.CalledProcessError as e:
1553+ log('Error unmounting {}\n{}'.format(mountpoint, e.output))
1554+ return False
1555+
1556+ if persist:
1557+ return fstab_remove(mountpoint)
1558+ return True
1559+
1560+
1561+def mounts():
1562+ """Get a list of all mounted volumes as [[mountpoint,device],[...]]"""
1563+ with open('/proc/mounts') as f:
1564+ # [['/mount/point','/dev/path'],[...]]
1565+ system_mounts = [m[1::-1] for m in [l.strip().split()
1566+ for l in f.readlines()]]
1567+ return system_mounts
1568+
1569+
1570+def fstab_mount(mountpoint):
1571+ """Mount filesystem using fstab"""
1572+ cmd_args = ['mount', mountpoint]
1573+ try:
1574+ subprocess.check_output(cmd_args)
1575+ except subprocess.CalledProcessError as e:
1576+ log('Error unmounting {}\n{}'.format(mountpoint, e.output))
1577+ return False
1578+ return True
1579+
1580+
1581+def file_hash(path, hash_type='md5'):
1582+ """
1583+ Generate a hash checksum of the contents of 'path' or None if not found.
1584+
1585+ :param str hash_type: Any hash alrgorithm supported by :mod:`hashlib`,
1586+ such as md5, sha1, sha256, sha512, etc.
1587+ """
1588+ if os.path.exists(path):
1589+ h = getattr(hashlib, hash_type)()
1590+ with open(path, 'rb') as source:
1591+ h.update(source.read())
1592+ return h.hexdigest()
1593+ else:
1594+ return None
1595+
1596+
1597+def path_hash(path):
1598+ """
1599+ Generate a hash checksum of all files matching 'path'. Standard wildcards
1600+ like '*' and '?' are supported, see documentation for the 'glob' module for
1601+ more information.
1602+
1603+ :return: dict: A { filename: hash } dictionary for all matched files.
1604+ Empty if none found.
1605+ """
1606+ return {
1607+ filename: file_hash(filename)
1608+ for filename in glob.iglob(path)
1609+ }
1610+
1611+
1612+def check_hash(path, checksum, hash_type='md5'):
1613+ """
1614+ Validate a file using a cryptographic checksum.
1615+
1616+ :param str checksum: Value of the checksum used to validate the file.
1617+ :param str hash_type: Hash algorithm used to generate `checksum`.
1618+ Can be any hash alrgorithm supported by :mod:`hashlib`,
1619+ such as md5, sha1, sha256, sha512, etc.
1620+ :raises ChecksumError: If the file fails the checksum
1621+
1622+ """
1623+ actual_checksum = file_hash(path, hash_type)
1624+ if checksum != actual_checksum:
1625+ raise ChecksumError("'%s' != '%s'" % (checksum, actual_checksum))
1626+
1627+
1628+class ChecksumError(ValueError):
1629+ pass
1630+
1631+
1632+def restart_on_change(restart_map, stopstart=False):
1633+ """Restart services based on configuration files changing
1634+
1635+ This function is used a decorator, for example::
1636+
1637+ @restart_on_change({
1638+ '/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ]
1639+ '/etc/apache/sites-enabled/*': [ 'apache2' ]
1640+ })
1641+ def config_changed():
1642+ pass # your code here
1643+
1644+ In this example, the cinder-api and cinder-volume services
1645+ would be restarted if /etc/ceph/ceph.conf is changed by the
1646+ ceph_client_changed function. The apache2 service would be
1647+ restarted if any file matching the pattern got changed, created
1648+ or removed. Standard wildcards are supported, see documentation
1649+ for the 'glob' module for more information.
1650+ """
1651+ def wrap(f):
1652+ def wrapped_f(*args, **kwargs):
1653+ checksums = {path: path_hash(path) for path in restart_map}
1654+ f(*args, **kwargs)
1655+ restarts = []
1656+ for path in restart_map:
1657+ if path_hash(path) != checksums[path]:
1658+ restarts += restart_map[path]
1659+ services_list = list(OrderedDict.fromkeys(restarts))
1660+ if not stopstart:
1661+ for service_name in services_list:
1662+ service('restart', service_name)
1663+ else:
1664+ for action in ['stop', 'start']:
1665+ for service_name in services_list:
1666+ service(action, service_name)
1667+ return wrapped_f
1668+ return wrap
1669+
1670+
1671+def lsb_release():
1672+ """Return /etc/lsb-release in a dict"""
1673+ d = {}
1674+ with open('/etc/lsb-release', 'r') as lsb:
1675+ for l in lsb:
1676+ k, v = l.split('=')
1677+ d[k.strip()] = v.strip()
1678+ return d
1679+
1680+
1681+def pwgen(length=None):
1682+ """Generate a random pasword."""
1683+ if length is None:
1684+ # A random length is ok to use a weak PRNG
1685+ length = random.choice(range(35, 45))
1686+ alphanumeric_chars = [
1687+ l for l in (string.ascii_letters + string.digits)
1688+ if l not in 'l0QD1vAEIOUaeiou']
1689+ # Use a crypto-friendly PRNG (e.g. /dev/urandom) for making the
1690+ # actual password
1691+ random_generator = random.SystemRandom()
1692+ random_chars = [
1693+ random_generator.choice(alphanumeric_chars) for _ in range(length)]
1694+ return(''.join(random_chars))
1695+
1696+
1697+def is_phy_iface(interface):
1698+ """Returns True if interface is not virtual, otherwise False."""
1699+ if interface:
1700+ sys_net = '/sys/class/net'
1701+ if os.path.isdir(sys_net):
1702+ for iface in glob.glob(os.path.join(sys_net, '*')):
1703+ if '/virtual/' in os.path.realpath(iface):
1704+ continue
1705+
1706+ if interface == os.path.basename(iface):
1707+ return True
1708+
1709+ return False
1710+
1711+
1712+def get_bond_master(interface):
1713+ """Returns bond master if interface is bond slave otherwise None.
1714+
1715+ NOTE: the provided interface is expected to be physical
1716+ """
1717+ if interface:
1718+ iface_path = '/sys/class/net/%s' % (interface)
1719+ if os.path.exists(iface_path):
1720+ if '/virtual/' in os.path.realpath(iface_path):
1721+ return None
1722+
1723+ master = os.path.join(iface_path, 'master')
1724+ if os.path.exists(master):
1725+ master = os.path.realpath(master)
1726+ # make sure it is a bond master
1727+ if os.path.exists(os.path.join(master, 'bonding')):
1728+ return os.path.basename(master)
1729+
1730+ return None
1731+
1732+
1733+def list_nics(nic_type=None):
1734+ '''Return a list of nics of given type(s)'''
1735+ if isinstance(nic_type, six.string_types):
1736+ int_types = [nic_type]
1737+ else:
1738+ int_types = nic_type
1739+
1740+ interfaces = []
1741+ if nic_type:
1742+ for int_type in int_types:
1743+ cmd = ['ip', 'addr', 'show', 'label', int_type + '*']
1744+ ip_output = subprocess.check_output(cmd).decode('UTF-8')
1745+ ip_output = ip_output.split('\n')
1746+ ip_output = (line for line in ip_output if line)
1747+ for line in ip_output:
1748+ if line.split()[1].startswith(int_type):
1749+ matched = re.search('.*: (' + int_type +
1750+ r'[0-9]+\.[0-9]+)@.*', line)
1751+ if matched:
1752+ iface = matched.groups()[0]
1753+ else:
1754+ iface = line.split()[1].replace(":", "")
1755+
1756+ if iface not in interfaces:
1757+ interfaces.append(iface)
1758+ else:
1759+ cmd = ['ip', 'a']
1760+ ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
1761+ ip_output = (line.strip() for line in ip_output if line)
1762+
1763+ key = re.compile('^[0-9]+:\s+(.+):')
1764+ for line in ip_output:
1765+ matched = re.search(key, line)
1766+ if matched:
1767+ iface = matched.group(1)
1768+ iface = iface.partition("@")[0]
1769+ if iface not in interfaces:
1770+ interfaces.append(iface)
1771+
1772+ return interfaces
1773+
1774+
1775+def set_nic_mtu(nic, mtu):
1776+ '''Set MTU on a network interface'''
1777+ cmd = ['ip', 'link', 'set', nic, 'mtu', mtu]
1778+ subprocess.check_call(cmd)
1779+
1780+
1781+def get_nic_mtu(nic):
1782+ cmd = ['ip', 'addr', 'show', nic]
1783+ ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
1784+ mtu = ""
1785+ for line in ip_output:
1786+ words = line.split()
1787+ if 'mtu' in words:
1788+ mtu = words[words.index("mtu") + 1]
1789+ return mtu
1790+
1791+
1792+def get_nic_hwaddr(nic):
1793+ cmd = ['ip', '-o', '-0', 'addr', 'show', nic]
1794+ ip_output = subprocess.check_output(cmd).decode('UTF-8')
1795+ hwaddr = ""
1796+ words = ip_output.split()
1797+ if 'link/ether' in words:
1798+ hwaddr = words[words.index('link/ether') + 1]
1799+ return hwaddr
1800+
1801+
1802+def cmp_pkgrevno(package, revno, pkgcache=None):
1803+ '''Compare supplied revno with the revno of the installed package
1804+
1805+ * 1 => Installed revno is greater than supplied arg
1806+ * 0 => Installed revno is the same as supplied arg
1807+ * -1 => Installed revno is less than supplied arg
1808+
1809+ This function imports apt_cache function from charmhelpers.fetch if
1810+ the pkgcache argument is None. Be sure to add charmhelpers.fetch if
1811+ you call this function, or pass an apt_pkg.Cache() instance.
1812+ '''
1813+ import apt_pkg
1814+ if not pkgcache:
1815+ from charmhelpers.fetch import apt_cache
1816+ pkgcache = apt_cache()
1817+ pkg = pkgcache[package]
1818+ return apt_pkg.version_compare(pkg.current_ver.ver_str, revno)
1819+
1820+
1821+@contextmanager
1822+def chdir(d):
1823+ cur = os.getcwd()
1824+ try:
1825+ yield os.chdir(d)
1826+ finally:
1827+ os.chdir(cur)
1828+
1829+
1830+def chownr(path, owner, group, follow_links=True):
1831+ uid = pwd.getpwnam(owner).pw_uid
1832+ gid = grp.getgrnam(group).gr_gid
1833+ if follow_links:
1834+ chown = os.chown
1835+ else:
1836+ chown = os.lchown
1837+
1838+ for root, dirs, files in os.walk(path):
1839+ for name in dirs + files:
1840+ full = os.path.join(root, name)
1841+ broken_symlink = os.path.lexists(full) and not os.path.exists(full)
1842+ if not broken_symlink:
1843+ chown(full, uid, gid)
1844+
1845+
1846+def lchownr(path, owner, group):
1847+ chownr(path, owner, group, follow_links=False)
1848
1849=== added file 'hooks/charmhelpers/core/hugepage.py'
1850--- hooks/charmhelpers/core/hugepage.py 1970-01-01 00:00:00 +0000
1851+++ hooks/charmhelpers/core/hugepage.py 2015-08-30 15:58:19 +0000
1852@@ -0,0 +1,62 @@
1853+# -*- coding: utf-8 -*-
1854+
1855+# Copyright 2014-2015 Canonical Limited.
1856+#
1857+# This file is part of charm-helpers.
1858+#
1859+# charm-helpers is free software: you can redistribute it and/or modify
1860+# it under the terms of the GNU Lesser General Public License version 3 as
1861+# published by the Free Software Foundation.
1862+#
1863+# charm-helpers is distributed in the hope that it will be useful,
1864+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1865+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1866+# GNU Lesser General Public License for more details.
1867+#
1868+# You should have received a copy of the GNU Lesser General Public License
1869+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1870+
1871+import yaml
1872+from charmhelpers.core import fstab
1873+from charmhelpers.core import sysctl
1874+from charmhelpers.core.host import (
1875+ add_group,
1876+ add_user_to_group,
1877+ fstab_mount,
1878+ mkdir,
1879+)
1880+
1881+
1882+def hugepage_support(user, group='hugetlb', nr_hugepages=256,
1883+ max_map_count=65536, mnt_point='/run/hugepages/kvm',
1884+ pagesize='2MB', mount=True):
1885+ """Enable hugepages on system.
1886+
1887+ Args:
1888+ user (str) -- Username to allow access to hugepages to
1889+ group (str) -- Group name to own hugepages
1890+ nr_hugepages (int) -- Number of pages to reserve
1891+ max_map_count (int) -- Number of Virtual Memory Areas a process can own
1892+ mnt_point (str) -- Directory to mount hugepages on
1893+ pagesize (str) -- Size of hugepages
1894+ mount (bool) -- Whether to Mount hugepages
1895+ """
1896+ group_info = add_group(group)
1897+ gid = group_info.gr_gid
1898+ add_user_to_group(user, group)
1899+ sysctl_settings = {
1900+ 'vm.nr_hugepages': nr_hugepages,
1901+ 'vm.max_map_count': max_map_count,
1902+ 'vm.hugetlb_shm_group': gid,
1903+ }
1904+ sysctl.create(yaml.dump(sysctl_settings), '/etc/sysctl.d/10-hugepage.conf')
1905+ mkdir(mnt_point, owner='root', group='root', perms=0o755, force=False)
1906+ lfstab = fstab.Fstab()
1907+ fstab_entry = lfstab.get_entry_by_attr('mountpoint', mnt_point)
1908+ if fstab_entry:
1909+ lfstab.remove_entry(fstab_entry)
1910+ entry = lfstab.Entry('nodev', mnt_point, 'hugetlbfs',
1911+ 'mode=1770,gid={},pagesize={}'.format(gid, pagesize), 0, 0)
1912+ lfstab.add_entry(entry)
1913+ if mount:
1914+ fstab_mount(mnt_point)
1915
1916=== added directory 'hooks/charmhelpers/core/services'
1917=== added file 'hooks/charmhelpers/core/services/__init__.py'
1918--- hooks/charmhelpers/core/services/__init__.py 1970-01-01 00:00:00 +0000
1919+++ hooks/charmhelpers/core/services/__init__.py 2015-08-30 15:58:19 +0000
1920@@ -0,0 +1,18 @@
1921+# Copyright 2014-2015 Canonical Limited.
1922+#
1923+# This file is part of charm-helpers.
1924+#
1925+# charm-helpers is free software: you can redistribute it and/or modify
1926+# it under the terms of the GNU Lesser General Public License version 3 as
1927+# published by the Free Software Foundation.
1928+#
1929+# charm-helpers is distributed in the hope that it will be useful,
1930+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1931+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1932+# GNU Lesser General Public License for more details.
1933+#
1934+# You should have received a copy of the GNU Lesser General Public License
1935+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1936+
1937+from .base import * # NOQA
1938+from .helpers import * # NOQA
1939
1940=== added file 'hooks/charmhelpers/core/services/base.py'
1941--- hooks/charmhelpers/core/services/base.py 1970-01-01 00:00:00 +0000
1942+++ hooks/charmhelpers/core/services/base.py 2015-08-30 15:58:19 +0000
1943@@ -0,0 +1,353 @@
1944+# Copyright 2014-2015 Canonical Limited.
1945+#
1946+# This file is part of charm-helpers.
1947+#
1948+# charm-helpers is free software: you can redistribute it and/or modify
1949+# it under the terms of the GNU Lesser General Public License version 3 as
1950+# published by the Free Software Foundation.
1951+#
1952+# charm-helpers is distributed in the hope that it will be useful,
1953+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1954+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1955+# GNU Lesser General Public License for more details.
1956+#
1957+# You should have received a copy of the GNU Lesser General Public License
1958+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1959+
1960+import os
1961+import json
1962+from inspect import getargspec
1963+from collections import Iterable, OrderedDict
1964+
1965+from charmhelpers.core import host
1966+from charmhelpers.core import hookenv
1967+
1968+
1969+__all__ = ['ServiceManager', 'ManagerCallback',
1970+ 'PortManagerCallback', 'open_ports', 'close_ports', 'manage_ports',
1971+ 'service_restart', 'service_stop']
1972+
1973+
1974+class ServiceManager(object):
1975+ def __init__(self, services=None):
1976+ """
1977+ Register a list of services, given their definitions.
1978+
1979+ Service definitions are dicts in the following formats (all keys except
1980+ 'service' are optional)::
1981+
1982+ {
1983+ "service": <service name>,
1984+ "required_data": <list of required data contexts>,
1985+ "provided_data": <list of provided data contexts>,
1986+ "data_ready": <one or more callbacks>,
1987+ "data_lost": <one or more callbacks>,
1988+ "start": <one or more callbacks>,
1989+ "stop": <one or more callbacks>,
1990+ "ports": <list of ports to manage>,
1991+ }
1992+
1993+ The 'required_data' list should contain dicts of required data (or
1994+ dependency managers that act like dicts and know how to collect the data).
1995+ Only when all items in the 'required_data' list are populated are the list
1996+ of 'data_ready' and 'start' callbacks executed. See `is_ready()` for more
1997+ information.
1998+
1999+ The 'provided_data' list should contain relation data providers, most likely
2000+ a subclass of :class:`charmhelpers.core.services.helpers.RelationContext`,
2001+ that will indicate a set of data to set on a given relation.
2002+
2003+ The 'data_ready' value should be either a single callback, or a list of
2004+ callbacks, to be called when all items in 'required_data' pass `is_ready()`.
2005+ Each callback will be called with the service name as the only parameter.
2006+ After all of the 'data_ready' callbacks are called, the 'start' callbacks
2007+ are fired.
2008+
2009+ The 'data_lost' value should be either a single callback, or a list of
2010+ callbacks, to be called when a 'required_data' item no longer passes
2011+ `is_ready()`. Each callback will be called with the service name as the
2012+ only parameter. After all of the 'data_lost' callbacks are called,
2013+ the 'stop' callbacks are fired.
2014+
2015+ The 'start' value should be either a single callback, or a list of
2016+ callbacks, to be called when starting the service, after the 'data_ready'
2017+ callbacks are complete. Each callback will be called with the service
2018+ name as the only parameter. This defaults to
2019+ `[host.service_start, services.open_ports]`.
2020+
2021+ The 'stop' value should be either a single callback, or a list of
2022+ callbacks, to be called when stopping the service. If the service is
2023+ being stopped because it no longer has all of its 'required_data', this
2024+ will be called after all of the 'data_lost' callbacks are complete.
2025+ Each callback will be called with the service name as the only parameter.
2026+ This defaults to `[services.close_ports, host.service_stop]`.
2027+
2028+ The 'ports' value should be a list of ports to manage. The default
2029+ 'start' handler will open the ports after the service is started,
2030+ and the default 'stop' handler will close the ports prior to stopping
2031+ the service.
2032+
2033+
2034+ Examples:
2035+
2036+ The following registers an Upstart service called bingod that depends on
2037+ a mongodb relation and which runs a custom `db_migrate` function prior to
2038+ restarting the service, and a Runit service called spadesd::
2039+
2040+ manager = services.ServiceManager([
2041+ {
2042+ 'service': 'bingod',
2043+ 'ports': [80, 443],
2044+ 'required_data': [MongoRelation(), config(), {'my': 'data'}],
2045+ 'data_ready': [
2046+ services.template(source='bingod.conf'),
2047+ services.template(source='bingod.ini',
2048+ target='/etc/bingod.ini',
2049+ owner='bingo', perms=0400),
2050+ ],
2051+ },
2052+ {
2053+ 'service': 'spadesd',
2054+ 'data_ready': services.template(source='spadesd_run.j2',
2055+ target='/etc/sv/spadesd/run',
2056+ perms=0555),
2057+ 'start': runit_start,
2058+ 'stop': runit_stop,
2059+ },
2060+ ])
2061+ manager.manage()
2062+ """
2063+ self._ready_file = os.path.join(hookenv.charm_dir(), 'READY-SERVICES.json')
2064+ self._ready = None
2065+ self.services = OrderedDict()
2066+ for service in services or []:
2067+ service_name = service['service']
2068+ self.services[service_name] = service
2069+
2070+ def manage(self):
2071+ """
2072+ Handle the current hook by doing The Right Thing with the registered services.
2073+ """
2074+ hookenv._run_atstart()
2075+ try:
2076+ hook_name = hookenv.hook_name()
2077+ if hook_name == 'stop':
2078+ self.stop_services()
2079+ else:
2080+ self.reconfigure_services()
2081+ self.provide_data()
2082+ except SystemExit as x:
2083+ if x.code is None or x.code == 0:
2084+ hookenv._run_atexit()
2085+ hookenv._run_atexit()
2086+
2087+ def provide_data(self):
2088+ """
2089+ Set the relation data for each provider in the ``provided_data`` list.
2090+
2091+ A provider must have a `name` attribute, which indicates which relation
2092+ to set data on, and a `provide_data()` method, which returns a dict of
2093+ data to set.
2094+
2095+ The `provide_data()` method can optionally accept two parameters:
2096+
2097+ * ``remote_service`` The name of the remote service that the data will
2098+ be provided to. The `provide_data()` method will be called once
2099+ for each connected service (not unit). This allows the method to
2100+ tailor its data to the given service.
2101+ * ``service_ready`` Whether or not the service definition had all of
2102+ its requirements met, and thus the ``data_ready`` callbacks run.
2103+
2104+ Note that the ``provided_data`` methods are now called **after** the
2105+ ``data_ready`` callbacks are run. This gives the ``data_ready`` callbacks
2106+ a chance to generate any data necessary for the providing to the remote
2107+ services.
2108+ """
2109+ for service_name, service in self.services.items():
2110+ service_ready = self.is_ready(service_name)
2111+ for provider in service.get('provided_data', []):
2112+ for relid in hookenv.relation_ids(provider.name):
2113+ units = hookenv.related_units(relid)
2114+ if not units:
2115+ continue
2116+ remote_service = units[0].split('/')[0]
2117+ argspec = getargspec(provider.provide_data)
2118+ if len(argspec.args) > 1:
2119+ data = provider.provide_data(remote_service, service_ready)
2120+ else:
2121+ data = provider.provide_data()
2122+ if data:
2123+ hookenv.relation_set(relid, data)
2124+
2125+ def reconfigure_services(self, *service_names):
2126+ """
2127+ Update all files for one or more registered services, and,
2128+ if ready, optionally restart them.
2129+
2130+ If no service names are given, reconfigures all registered services.
2131+ """
2132+ for service_name in service_names or self.services.keys():
2133+ if self.is_ready(service_name):
2134+ self.fire_event('data_ready', service_name)
2135+ self.fire_event('start', service_name, default=[
2136+ service_restart,
2137+ manage_ports])
2138+ self.save_ready(service_name)
2139+ else:
2140+ if self.was_ready(service_name):
2141+ self.fire_event('data_lost', service_name)
2142+ self.fire_event('stop', service_name, default=[
2143+ manage_ports,
2144+ service_stop])
2145+ self.save_lost(service_name)
2146+
2147+ def stop_services(self, *service_names):
2148+ """
2149+ Stop one or more registered services, by name.
2150+
2151+ If no service names are given, stops all registered services.
2152+ """
2153+ for service_name in service_names or self.services.keys():
2154+ self.fire_event('stop', service_name, default=[
2155+ manage_ports,
2156+ service_stop])
2157+
2158+ def get_service(self, service_name):
2159+ """
2160+ Given the name of a registered service, return its service definition.
2161+ """
2162+ service = self.services.get(service_name)
2163+ if not service:
2164+ raise KeyError('Service not registered: %s' % service_name)
2165+ return service
2166+
2167+ def fire_event(self, event_name, service_name, default=None):
2168+ """
2169+ Fire a data_ready, data_lost, start, or stop event on a given service.
2170+ """
2171+ service = self.get_service(service_name)
2172+ callbacks = service.get(event_name, default)
2173+ if not callbacks:
2174+ return
2175+ if not isinstance(callbacks, Iterable):
2176+ callbacks = [callbacks]
2177+ for callback in callbacks:
2178+ if isinstance(callback, ManagerCallback):
2179+ callback(self, service_name, event_name)
2180+ else:
2181+ callback(service_name)
2182+
2183+ def is_ready(self, service_name):
2184+ """
2185+ Determine if a registered service is ready, by checking its 'required_data'.
2186+
2187+ A 'required_data' item can be any mapping type, and is considered ready
2188+ if `bool(item)` evaluates as True.
2189+ """
2190+ service = self.get_service(service_name)
2191+ reqs = service.get('required_data', [])
2192+ return all(bool(req) for req in reqs)
2193+
2194+ def _load_ready_file(self):
2195+ if self._ready is not None:
2196+ return
2197+ if os.path.exists(self._ready_file):
2198+ with open(self._ready_file) as fp:
2199+ self._ready = set(json.load(fp))
2200+ else:
2201+ self._ready = set()
2202+
2203+ def _save_ready_file(self):
2204+ if self._ready is None:
2205+ return
2206+ with open(self._ready_file, 'w') as fp:
2207+ json.dump(list(self._ready), fp)
2208+
2209+ def save_ready(self, service_name):
2210+ """
2211+ Save an indicator that the given service is now data_ready.
2212+ """
2213+ self._load_ready_file()
2214+ self._ready.add(service_name)
2215+ self._save_ready_file()
2216+
2217+ def save_lost(self, service_name):
2218+ """
2219+ Save an indicator that the given service is no longer data_ready.
2220+ """
2221+ self._load_ready_file()
2222+ self._ready.discard(service_name)
2223+ self._save_ready_file()
2224+
2225+ def was_ready(self, service_name):
2226+ """
2227+ Determine if the given service was previously data_ready.
2228+ """
2229+ self._load_ready_file()
2230+ return service_name in self._ready
2231+
2232+
2233+class ManagerCallback(object):
2234+ """
2235+ Special case of a callback that takes the `ServiceManager` instance
2236+ in addition to the service name.
2237+
2238+ Subclasses should implement `__call__` which should accept three parameters:
2239+
2240+ * `manager` The `ServiceManager` instance
2241+ * `service_name` The name of the service it's being triggered for
2242+ * `event_name` The name of the event that this callback is handling
2243+ """
2244+ def __call__(self, manager, service_name, event_name):
2245+ raise NotImplementedError()
2246+
2247+
2248+class PortManagerCallback(ManagerCallback):
2249+ """
2250+ Callback class that will open or close ports, for use as either
2251+ a start or stop action.
2252+ """
2253+ def __call__(self, manager, service_name, event_name):
2254+ service = manager.get_service(service_name)
2255+ new_ports = service.get('ports', [])
2256+ port_file = os.path.join(hookenv.charm_dir(), '.{}.ports'.format(service_name))
2257+ if os.path.exists(port_file):
2258+ with open(port_file) as fp:
2259+ old_ports = fp.read().split(',')
2260+ for old_port in old_ports:
2261+ if bool(old_port):
2262+ old_port = int(old_port)
2263+ if old_port not in new_ports:
2264+ hookenv.close_port(old_port)
2265+ with open(port_file, 'w') as fp:
2266+ fp.write(','.join(str(port) for port in new_ports))
2267+ for port in new_ports:
2268+ if event_name == 'start':
2269+ hookenv.open_port(port)
2270+ elif event_name == 'stop':
2271+ hookenv.close_port(port)
2272+
2273+
2274+def service_stop(service_name):
2275+ """
2276+ Wrapper around host.service_stop to prevent spurious "unknown service"
2277+ messages in the logs.
2278+ """
2279+ if host.service_running(service_name):
2280+ host.service_stop(service_name)
2281+
2282+
2283+def service_restart(service_name):
2284+ """
2285+ Wrapper around host.service_restart to prevent spurious "unknown service"
2286+ messages in the logs.
2287+ """
2288+ if host.service_available(service_name):
2289+ if host.service_running(service_name):
2290+ host.service_restart(service_name)
2291+ else:
2292+ host.service_start(service_name)
2293+
2294+
2295+# Convenience aliases
2296+open_ports = close_ports = manage_ports = PortManagerCallback()
2297
2298=== added file 'hooks/charmhelpers/core/services/helpers.py'
2299--- hooks/charmhelpers/core/services/helpers.py 1970-01-01 00:00:00 +0000
2300+++ hooks/charmhelpers/core/services/helpers.py 2015-08-30 15:58:19 +0000
2301@@ -0,0 +1,283 @@
2302+# Copyright 2014-2015 Canonical Limited.
2303+#
2304+# This file is part of charm-helpers.
2305+#
2306+# charm-helpers is free software: you can redistribute it and/or modify
2307+# it under the terms of the GNU Lesser General Public License version 3 as
2308+# published by the Free Software Foundation.
2309+#
2310+# charm-helpers is distributed in the hope that it will be useful,
2311+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2312+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2313+# GNU Lesser General Public License for more details.
2314+#
2315+# You should have received a copy of the GNU Lesser General Public License
2316+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2317+
2318+import os
2319+import yaml
2320+
2321+from charmhelpers.core import hookenv
2322+from charmhelpers.core import host
2323+from charmhelpers.core import templating
2324+
2325+from charmhelpers.core.services.base import ManagerCallback
2326+
2327+
2328+__all__ = ['RelationContext', 'TemplateCallback',
2329+ 'render_template', 'template']
2330+
2331+
2332+class RelationContext(dict):
2333+ """
2334+ Base class for a context generator that gets relation data from juju.
2335+
2336+ Subclasses must provide the attributes `name`, which is the name of the
2337+ interface of interest, `interface`, which is the type of the interface of
2338+ interest, and `required_keys`, which is the set of keys required for the
2339+ relation to be considered complete. The data for all interfaces matching
2340+ the `name` attribute that are complete will used to populate the dictionary
2341+ values (see `get_data`, below).
2342+
2343+ The generated context will be namespaced under the relation :attr:`name`,
2344+ to prevent potential naming conflicts.
2345+
2346+ :param str name: Override the relation :attr:`name`, since it can vary from charm to charm
2347+ :param list additional_required_keys: Extend the list of :attr:`required_keys`
2348+ """
2349+ name = None
2350+ interface = None
2351+
2352+ def __init__(self, name=None, additional_required_keys=None):
2353+ if not hasattr(self, 'required_keys'):
2354+ self.required_keys = []
2355+
2356+ if name is not None:
2357+ self.name = name
2358+ if additional_required_keys:
2359+ self.required_keys.extend(additional_required_keys)
2360+ self.get_data()
2361+
2362+ def __bool__(self):
2363+ """
2364+ Returns True if all of the required_keys are available.
2365+ """
2366+ return self.is_ready()
2367+
2368+ __nonzero__ = __bool__
2369+
2370+ def __repr__(self):
2371+ return super(RelationContext, self).__repr__()
2372+
2373+ def is_ready(self):
2374+ """
2375+ Returns True if all of the `required_keys` are available from any units.
2376+ """
2377+ ready = len(self.get(self.name, [])) > 0
2378+ if not ready:
2379+ hookenv.log('Incomplete relation: {}'.format(self.__class__.__name__), hookenv.DEBUG)
2380+ return ready
2381+
2382+ def _is_ready(self, unit_data):
2383+ """
2384+ Helper method that tests a set of relation data and returns True if
2385+ all of the `required_keys` are present.
2386+ """
2387+ return set(unit_data.keys()).issuperset(set(self.required_keys))
2388+
2389+ def get_data(self):
2390+ """
2391+ Retrieve the relation data for each unit involved in a relation and,
2392+ if complete, store it in a list under `self[self.name]`. This
2393+ is automatically called when the RelationContext is instantiated.
2394+
2395+ The units are sorted lexographically first by the service ID, then by
2396+ the unit ID. Thus, if an interface has two other services, 'db:1'
2397+ and 'db:2', with 'db:1' having two units, 'wordpress/0' and 'wordpress/1',
2398+ and 'db:2' having one unit, 'mediawiki/0', all of which have a complete
2399+ set of data, the relation data for the units will be stored in the
2400+ order: 'wordpress/0', 'wordpress/1', 'mediawiki/0'.
2401+
2402+ If you only care about a single unit on the relation, you can just
2403+ access it as `{{ interface[0]['key'] }}`. However, if you can at all
2404+ support multiple units on a relation, you should iterate over the list,
2405+ like::
2406+
2407+ {% for unit in interface -%}
2408+ {{ unit['key'] }}{% if not loop.last %},{% endif %}
2409+ {%- endfor %}
2410+
2411+ Note that since all sets of relation data from all related services and
2412+ units are in a single list, if you need to know which service or unit a
2413+ set of data came from, you'll need to extend this class to preserve
2414+ that information.
2415+ """
2416+ if not hookenv.relation_ids(self.name):
2417+ return
2418+
2419+ ns = self.setdefault(self.name, [])
2420+ for rid in sorted(hookenv.relation_ids(self.name)):
2421+ for unit in sorted(hookenv.related_units(rid)):
2422+ reldata = hookenv.relation_get(rid=rid, unit=unit)
2423+ if self._is_ready(reldata):
2424+ ns.append(reldata)
2425+
2426+ def provide_data(self):
2427+ """
2428+ Return data to be relation_set for this interface.
2429+ """
2430+ return {}
2431+
2432+
2433+class MysqlRelation(RelationContext):
2434+ """
2435+ Relation context for the `mysql` interface.
2436+
2437+ :param str name: Override the relation :attr:`name`, since it can vary from charm to charm
2438+ :param list additional_required_keys: Extend the list of :attr:`required_keys`
2439+ """
2440+ name = 'db'
2441+ interface = 'mysql'
2442+
2443+ def __init__(self, *args, **kwargs):
2444+ self.required_keys = ['host', 'user', 'password', 'database']
2445+ RelationContext.__init__(self, *args, **kwargs)
2446+
2447+
2448+class HttpRelation(RelationContext):
2449+ """
2450+ Relation context for the `http` interface.
2451+
2452+ :param str name: Override the relation :attr:`name`, since it can vary from charm to charm
2453+ :param list additional_required_keys: Extend the list of :attr:`required_keys`
2454+ """
2455+ name = 'website'
2456+ interface = 'http'
2457+
2458+ def __init__(self, *args, **kwargs):
2459+ self.required_keys = ['host', 'port']
2460+ RelationContext.__init__(self, *args, **kwargs)
2461+
2462+ def provide_data(self):
2463+ return {
2464+ 'host': hookenv.unit_get('private-address'),
2465+ 'port': 80,
2466+ }
2467+
2468+
2469+class RequiredConfig(dict):
2470+ """
2471+ Data context that loads config options with one or more mandatory options.
2472+
2473+ Once the required options have been changed from their default values, all
2474+ config options will be available, namespaced under `config` to prevent
2475+ potential naming conflicts (for example, between a config option and a
2476+ relation property).
2477+
2478+ :param list *args: List of options that must be changed from their default values.
2479+ """
2480+
2481+ def __init__(self, *args):
2482+ self.required_options = args
2483+ self['config'] = hookenv.config()
2484+ with open(os.path.join(hookenv.charm_dir(), 'config.yaml')) as fp:
2485+ self.config = yaml.load(fp).get('options', {})
2486+
2487+ def __bool__(self):
2488+ for option in self.required_options:
2489+ if option not in self['config']:
2490+ return False
2491+ current_value = self['config'][option]
2492+ default_value = self.config[option].get('default')
2493+ if current_value == default_value:
2494+ return False
2495+ if current_value in (None, '') and default_value in (None, ''):
2496+ return False
2497+ return True
2498+
2499+ def __nonzero__(self):
2500+ return self.__bool__()
2501+
2502+
2503+class StoredContext(dict):
2504+ """
2505+ A data context that always returns the data that it was first created with.
2506+
2507+ This is useful to do a one-time generation of things like passwords, that
2508+ will thereafter use the same value that was originally generated, instead
2509+ of generating a new value each time it is run.
2510+ """
2511+ def __init__(self, file_name, config_data):
2512+ """
2513+ If the file exists, populate `self` with the data from the file.
2514+ Otherwise, populate with the given data and persist it to the file.
2515+ """
2516+ if os.path.exists(file_name):
2517+ self.update(self.read_context(file_name))
2518+ else:
2519+ self.store_context(file_name, config_data)
2520+ self.update(config_data)
2521+
2522+ def store_context(self, file_name, config_data):
2523+ if not os.path.isabs(file_name):
2524+ file_name = os.path.join(hookenv.charm_dir(), file_name)
2525+ with open(file_name, 'w') as file_stream:
2526+ os.fchmod(file_stream.fileno(), 0o600)
2527+ yaml.dump(config_data, file_stream)
2528+
2529+ def read_context(self, file_name):
2530+ if not os.path.isabs(file_name):
2531+ file_name = os.path.join(hookenv.charm_dir(), file_name)
2532+ with open(file_name, 'r') as file_stream:
2533+ data = yaml.load(file_stream)
2534+ if not data:
2535+ raise OSError("%s is empty" % file_name)
2536+ return data
2537+
2538+
2539+class TemplateCallback(ManagerCallback):
2540+ """
2541+ Callback class that will render a Jinja2 template, for use as a ready
2542+ action.
2543+
2544+ :param str source: The template source file, relative to
2545+ `$CHARM_DIR/templates`
2546+
2547+ :param str target: The target to write the rendered template to
2548+ :param str owner: The owner of the rendered file
2549+ :param str group: The group of the rendered file
2550+ :param int perms: The permissions of the rendered file
2551+ :param partial on_change_action: functools partial to be executed when
2552+ rendered file changes
2553+ """
2554+ def __init__(self, source, target,
2555+ owner='root', group='root', perms=0o444,
2556+ on_change_action=None):
2557+ self.source = source
2558+ self.target = target
2559+ self.owner = owner
2560+ self.group = group
2561+ self.perms = perms
2562+ self.on_change_action = on_change_action
2563+
2564+ def __call__(self, manager, service_name, event_name):
2565+ pre_checksum = ''
2566+ if self.on_change_action and os.path.isfile(self.target):
2567+ pre_checksum = host.file_hash(self.target)
2568+ service = manager.get_service(service_name)
2569+ context = {}
2570+ for ctx in service.get('required_data', []):
2571+ context.update(ctx)
2572+ templating.render(self.source, self.target, context,
2573+ self.owner, self.group, self.perms)
2574+ if self.on_change_action:
2575+ if pre_checksum == host.file_hash(self.target):
2576+ hookenv.log(
2577+ 'No change detected: {}'.format(self.target),
2578+ hookenv.DEBUG)
2579+ else:
2580+ self.on_change_action()
2581+
2582+
2583+# Convenience aliases for templates
2584+render_template = template = TemplateCallback
2585
2586=== added file 'hooks/charmhelpers/core/strutils.py'
2587--- hooks/charmhelpers/core/strutils.py 1970-01-01 00:00:00 +0000
2588+++ hooks/charmhelpers/core/strutils.py 2015-08-30 15:58:19 +0000
2589@@ -0,0 +1,42 @@
2590+#!/usr/bin/env python
2591+# -*- coding: utf-8 -*-
2592+
2593+# Copyright 2014-2015 Canonical Limited.
2594+#
2595+# This file is part of charm-helpers.
2596+#
2597+# charm-helpers is free software: you can redistribute it and/or modify
2598+# it under the terms of the GNU Lesser General Public License version 3 as
2599+# published by the Free Software Foundation.
2600+#
2601+# charm-helpers is distributed in the hope that it will be useful,
2602+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2603+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2604+# GNU Lesser General Public License for more details.
2605+#
2606+# You should have received a copy of the GNU Lesser General Public License
2607+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2608+
2609+import six
2610+
2611+
2612+def bool_from_string(value):
2613+ """Interpret string value as boolean.
2614+
2615+ Returns True if value translates to True otherwise False.
2616+ """
2617+ if isinstance(value, six.string_types):
2618+ value = six.text_type(value)
2619+ else:
2620+ msg = "Unable to interpret non-string value '%s' as boolean" % (value)
2621+ raise ValueError(msg)
2622+
2623+ value = value.strip().lower()
2624+
2625+ if value in ['y', 'yes', 'true', 't', 'on']:
2626+ return True
2627+ elif value in ['n', 'no', 'false', 'f', 'off']:
2628+ return False
2629+
2630+ msg = "Unable to interpret string value '%s' as boolean" % (value)
2631+ raise ValueError(msg)
2632
2633=== added file 'hooks/charmhelpers/core/sysctl.py'
2634--- hooks/charmhelpers/core/sysctl.py 1970-01-01 00:00:00 +0000
2635+++ hooks/charmhelpers/core/sysctl.py 2015-08-30 15:58:19 +0000
2636@@ -0,0 +1,56 @@
2637+#!/usr/bin/env python
2638+# -*- coding: utf-8 -*-
2639+
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 yaml
2657+
2658+from subprocess import check_call
2659+
2660+from charmhelpers.core.hookenv import (
2661+ log,
2662+ DEBUG,
2663+ ERROR,
2664+)
2665+
2666+__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
2667+
2668+
2669+def create(sysctl_dict, sysctl_file):
2670+ """Creates a sysctl.conf file from a YAML associative array
2671+
2672+ :param sysctl_dict: a YAML-formatted string of sysctl options eg "{ 'kernel.max_pid': 1337 }"
2673+ :type sysctl_dict: str
2674+ :param sysctl_file: path to the sysctl file to be saved
2675+ :type sysctl_file: str or unicode
2676+ :returns: None
2677+ """
2678+ try:
2679+ sysctl_dict_parsed = yaml.safe_load(sysctl_dict)
2680+ except yaml.YAMLError:
2681+ log("Error parsing YAML sysctl_dict: {}".format(sysctl_dict),
2682+ level=ERROR)
2683+ return
2684+
2685+ with open(sysctl_file, "w") as fd:
2686+ for key, value in sysctl_dict_parsed.items():
2687+ fd.write("{}={}\n".format(key, value))
2688+
2689+ log("Updating sysctl_file: %s values: %s" % (sysctl_file, sysctl_dict_parsed),
2690+ level=DEBUG)
2691+
2692+ check_call(["sysctl", "-p", sysctl_file])
2693
2694=== added file 'hooks/charmhelpers/core/templating.py'
2695--- hooks/charmhelpers/core/templating.py 1970-01-01 00:00:00 +0000
2696+++ hooks/charmhelpers/core/templating.py 2015-08-30 15:58:19 +0000
2697@@ -0,0 +1,68 @@
2698+# Copyright 2014-2015 Canonical Limited.
2699+#
2700+# This file is part of charm-helpers.
2701+#
2702+# charm-helpers is free software: you can redistribute it and/or modify
2703+# it under the terms of the GNU Lesser General Public License version 3 as
2704+# published by the Free Software Foundation.
2705+#
2706+# charm-helpers is distributed in the hope that it will be useful,
2707+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2708+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2709+# GNU Lesser General Public License for more details.
2710+#
2711+# You should have received a copy of the GNU Lesser General Public License
2712+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2713+
2714+import os
2715+
2716+from charmhelpers.core import host
2717+from charmhelpers.core import hookenv
2718+
2719+
2720+def render(source, target, context, owner='root', group='root',
2721+ perms=0o444, templates_dir=None, encoding='UTF-8'):
2722+ """
2723+ Render a template.
2724+
2725+ The `source` path, if not absolute, is relative to the `templates_dir`.
2726+
2727+ The `target` path should be absolute.
2728+
2729+ The context should be a dict containing the values to be replaced in the
2730+ template.
2731+
2732+ The `owner`, `group`, and `perms` options will be passed to `write_file`.
2733+
2734+ If omitted, `templates_dir` defaults to the `templates` folder in the charm.
2735+
2736+ Note: Using this requires python-jinja2; if it is not installed, calling
2737+ this will attempt to use charmhelpers.fetch.apt_install to install it.
2738+ """
2739+ try:
2740+ from jinja2 import FileSystemLoader, Environment, exceptions
2741+ except ImportError:
2742+ try:
2743+ from charmhelpers.fetch import apt_install
2744+ except ImportError:
2745+ hookenv.log('Could not import jinja2, and could not import '
2746+ 'charmhelpers.fetch to install it',
2747+ level=hookenv.ERROR)
2748+ raise
2749+ apt_install('python-jinja2', fatal=True)
2750+ from jinja2 import FileSystemLoader, Environment, exceptions
2751+
2752+ if templates_dir is None:
2753+ templates_dir = os.path.join(hookenv.charm_dir(), 'templates')
2754+ loader = Environment(loader=FileSystemLoader(templates_dir))
2755+ try:
2756+ source = source
2757+ template = loader.get_template(source)
2758+ except exceptions.TemplateNotFound as e:
2759+ hookenv.log('Could not load template %s from %s.' %
2760+ (source, templates_dir),
2761+ level=hookenv.ERROR)
2762+ raise e
2763+ content = template.render(context)
2764+ host.mkdir(os.path.dirname(target), owner, group, perms=0o755)
2765+ host.write_file(target, content.encode(encoding), owner, group, perms)
2766
2767=== added file 'hooks/charmhelpers/core/unitdata.py'
2768--- hooks/charmhelpers/core/unitdata.py 1970-01-01 00:00:00 +0000
2769+++ hooks/charmhelpers/core/unitdata.py 2015-08-30 15:58:19 +0000
2770@@ -0,0 +1,521 @@
2771+#!/usr/bin/env python
2772+# -*- coding: utf-8 -*-
2773+#
2774+# Copyright 2014-2015 Canonical Limited.
2775+#
2776+# This file is part of charm-helpers.
2777+#
2778+# charm-helpers is free software: you can redistribute it and/or modify
2779+# it under the terms of the GNU Lesser General Public License version 3 as
2780+# published by the Free Software Foundation.
2781+#
2782+# charm-helpers is distributed in the hope that it will be useful,
2783+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2784+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2785+# GNU Lesser General Public License for more details.
2786+#
2787+# You should have received a copy of the GNU Lesser General Public License
2788+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2789+#
2790+#
2791+# Authors:
2792+# Kapil Thangavelu <kapil.foss@gmail.com>
2793+#
2794+"""
2795+Intro
2796+-----
2797+
2798+A simple way to store state in units. This provides a key value
2799+storage with support for versioned, transactional operation,
2800+and can calculate deltas from previous values to simplify unit logic
2801+when processing changes.
2802+
2803+
2804+Hook Integration
2805+----------------
2806+
2807+There are several extant frameworks for hook execution, including
2808+
2809+ - charmhelpers.core.hookenv.Hooks
2810+ - charmhelpers.core.services.ServiceManager
2811+
2812+The storage classes are framework agnostic, one simple integration is
2813+via the HookData contextmanager. It will record the current hook
2814+execution environment (including relation data, config data, etc.),
2815+setup a transaction and allow easy access to the changes from
2816+previously seen values. One consequence of the integration is the
2817+reservation of particular keys ('rels', 'unit', 'env', 'config',
2818+'charm_revisions') for their respective values.
2819+
2820+Here's a fully worked integration example using hookenv.Hooks::
2821+
2822+ from charmhelper.core import hookenv, unitdata
2823+
2824+ hook_data = unitdata.HookData()
2825+ db = unitdata.kv()
2826+ hooks = hookenv.Hooks()
2827+
2828+ @hooks.hook
2829+ def config_changed():
2830+ # Print all changes to configuration from previously seen
2831+ # values.
2832+ for changed, (prev, cur) in hook_data.conf.items():
2833+ print('config changed', changed,
2834+ 'previous value', prev,
2835+ 'current value', cur)
2836+
2837+ # Get some unit specific bookeeping
2838+ if not db.get('pkg_key'):
2839+ key = urllib.urlopen('https://example.com/pkg_key').read()
2840+ db.set('pkg_key', key)
2841+
2842+ # Directly access all charm config as a mapping.
2843+ conf = db.getrange('config', True)
2844+
2845+ # Directly access all relation data as a mapping
2846+ rels = db.getrange('rels', True)
2847+
2848+ if __name__ == '__main__':
2849+ with hook_data():
2850+ hook.execute()
2851+
2852+
2853+A more basic integration is via the hook_scope context manager which simply
2854+manages transaction scope (and records hook name, and timestamp)::
2855+
2856+ >>> from unitdata import kv
2857+ >>> db = kv()
2858+ >>> with db.hook_scope('install'):
2859+ ... # do work, in transactional scope.
2860+ ... db.set('x', 1)
2861+ >>> db.get('x')
2862+ 1
2863+
2864+
2865+Usage
2866+-----
2867+
2868+Values are automatically json de/serialized to preserve basic typing
2869+and complex data struct capabilities (dicts, lists, ints, booleans, etc).
2870+
2871+Individual values can be manipulated via get/set::
2872+
2873+ >>> kv.set('y', True)
2874+ >>> kv.get('y')
2875+ True
2876+
2877+ # We can set complex values (dicts, lists) as a single key.
2878+ >>> kv.set('config', {'a': 1, 'b': True'})
2879+
2880+ # Also supports returning dictionaries as a record which
2881+ # provides attribute access.
2882+ >>> config = kv.get('config', record=True)
2883+ >>> config.b
2884+ True
2885+
2886+
2887+Groups of keys can be manipulated with update/getrange::
2888+
2889+ >>> kv.update({'z': 1, 'y': 2}, prefix="gui.")
2890+ >>> kv.getrange('gui.', strip=True)
2891+ {'z': 1, 'y': 2}
2892+
2893+When updating values, its very helpful to understand which values
2894+have actually changed and how have they changed. The storage
2895+provides a delta method to provide for this::
2896+
2897+ >>> data = {'debug': True, 'option': 2}
2898+ >>> delta = kv.delta(data, 'config.')
2899+ >>> delta.debug.previous
2900+ None
2901+ >>> delta.debug.current
2902+ True
2903+ >>> delta
2904+ {'debug': (None, True), 'option': (None, 2)}
2905+
2906+Note the delta method does not persist the actual change, it needs to
2907+be explicitly saved via 'update' method::
2908+
2909+ >>> kv.update(data, 'config.')
2910+
2911+Values modified in the context of a hook scope retain historical values
2912+associated to the hookname.
2913+
2914+ >>> with db.hook_scope('config-changed'):
2915+ ... db.set('x', 42)
2916+ >>> db.gethistory('x')
2917+ [(1, u'x', 1, u'install', u'2015-01-21T16:49:30.038372'),
2918+ (2, u'x', 42, u'config-changed', u'2015-01-21T16:49:30.038786')]
2919+
2920+"""
2921+
2922+import collections
2923+import contextlib
2924+import datetime
2925+import itertools
2926+import json
2927+import os
2928+import pprint
2929+import sqlite3
2930+import sys
2931+
2932+__author__ = 'Kapil Thangavelu <kapil.foss@gmail.com>'
2933+
2934+
2935+class Storage(object):
2936+ """Simple key value database for local unit state within charms.
2937+
2938+ Modifications are not persisted unless :meth:`flush` is called.
2939+
2940+ To support dicts, lists, integer, floats, and booleans values
2941+ are automatically json encoded/decoded.
2942+ """
2943+ def __init__(self, path=None):
2944+ self.db_path = path
2945+ if path is None:
2946+ if 'UNIT_STATE_DB' in os.environ:
2947+ self.db_path = os.environ['UNIT_STATE_DB']
2948+ else:
2949+ self.db_path = os.path.join(
2950+ os.environ.get('CHARM_DIR', ''), '.unit-state.db')
2951+ self.conn = sqlite3.connect('%s' % self.db_path)
2952+ self.cursor = self.conn.cursor()
2953+ self.revision = None
2954+ self._closed = False
2955+ self._init()
2956+
2957+ def close(self):
2958+ if self._closed:
2959+ return
2960+ self.flush(False)
2961+ self.cursor.close()
2962+ self.conn.close()
2963+ self._closed = True
2964+
2965+ def get(self, key, default=None, record=False):
2966+ self.cursor.execute('select data from kv where key=?', [key])
2967+ result = self.cursor.fetchone()
2968+ if not result:
2969+ return default
2970+ if record:
2971+ return Record(json.loads(result[0]))
2972+ return json.loads(result[0])
2973+
2974+ def getrange(self, key_prefix, strip=False):
2975+ """
2976+ Get a range of keys starting with a common prefix as a mapping of
2977+ keys to values.
2978+
2979+ :param str key_prefix: Common prefix among all keys
2980+ :param bool strip: Optionally strip the common prefix from the key
2981+ names in the returned dict
2982+ :return dict: A (possibly empty) dict of key-value mappings
2983+ """
2984+ self.cursor.execute("select key, data from kv where key like ?",
2985+ ['%s%%' % key_prefix])
2986+ result = self.cursor.fetchall()
2987+
2988+ if not result:
2989+ return {}
2990+ if not strip:
2991+ key_prefix = ''
2992+ return dict([
2993+ (k[len(key_prefix):], json.loads(v)) for k, v in result])
2994+
2995+ def update(self, mapping, prefix=""):
2996+ """
2997+ Set the values of multiple keys at once.
2998+
2999+ :param dict mapping: Mapping of keys to values
3000+ :param str prefix: Optional prefix to apply to all keys in `mapping`
3001+ before setting
3002+ """
3003+ for k, v in mapping.items():
3004+ self.set("%s%s" % (prefix, k), v)
3005+
3006+ def unset(self, key):
3007+ """
3008+ Remove a key from the database entirely.
3009+ """
3010+ self.cursor.execute('delete from kv where key=?', [key])
3011+ if self.revision and self.cursor.rowcount:
3012+ self.cursor.execute(
3013+ 'insert into kv_revisions values (?, ?, ?)',
3014+ [key, self.revision, json.dumps('DELETED')])
3015+
3016+ def unsetrange(self, keys=None, prefix=""):
3017+ """
3018+ Remove a range of keys starting with a common prefix, from the database
3019+ entirely.
3020+
3021+ :param list keys: List of keys to remove.
3022+ :param str prefix: Optional prefix to apply to all keys in ``keys``
3023+ before removing.
3024+ """
3025+ if keys is not None:
3026+ keys = ['%s%s' % (prefix, key) for key in keys]
3027+ self.cursor.execute('delete from kv where key in (%s)' % ','.join(['?'] * len(keys)), keys)
3028+ if self.revision and self.cursor.rowcount:
3029+ self.cursor.execute(
3030+ 'insert into kv_revisions values %s' % ','.join(['(?, ?, ?)'] * len(keys)),
3031+ list(itertools.chain.from_iterable((key, self.revision, json.dumps('DELETED')) for key in keys)))
3032+ else:
3033+ self.cursor.execute('delete from kv where key like ?',
3034+ ['%s%%' % prefix])
3035+ if self.revision and self.cursor.rowcount:
3036+ self.cursor.execute(
3037+ 'insert into kv_revisions values (?, ?, ?)',
3038+ ['%s%%' % prefix, self.revision, json.dumps('DELETED')])
3039+
3040+ def set(self, key, value):
3041+ """
3042+ Set a value in the database.
3043+
3044+ :param str key: Key to set the value for
3045+ :param value: Any JSON-serializable value to be set
3046+ """
3047+ serialized = json.dumps(value)
3048+
3049+ self.cursor.execute('select data from kv where key=?', [key])
3050+ exists = self.cursor.fetchone()
3051+
3052+ # Skip mutations to the same value
3053+ if exists:
3054+ if exists[0] == serialized:
3055+ return value
3056+
3057+ if not exists:
3058+ self.cursor.execute(
3059+ 'insert into kv (key, data) values (?, ?)',
3060+ (key, serialized))
3061+ else:
3062+ self.cursor.execute('''
3063+ update kv
3064+ set data = ?
3065+ where key = ?''', [serialized, key])
3066+
3067+ # Save
3068+ if not self.revision:
3069+ return value
3070+
3071+ self.cursor.execute(
3072+ 'select 1 from kv_revisions where key=? and revision=?',
3073+ [key, self.revision])
3074+ exists = self.cursor.fetchone()
3075+
3076+ if not exists:
3077+ self.cursor.execute(
3078+ '''insert into kv_revisions (
3079+ revision, key, data) values (?, ?, ?)''',
3080+ (self.revision, key, serialized))
3081+ else:
3082+ self.cursor.execute(
3083+ '''
3084+ update kv_revisions
3085+ set data = ?
3086+ where key = ?
3087+ and revision = ?''',
3088+ [serialized, key, self.revision])
3089+
3090+ return value
3091+
3092+ def delta(self, mapping, prefix):
3093+ """
3094+ return a delta containing values that have changed.
3095+ """
3096+ previous = self.getrange(prefix, strip=True)
3097+ if not previous:
3098+ pk = set()
3099+ else:
3100+ pk = set(previous.keys())
3101+ ck = set(mapping.keys())
3102+ delta = DeltaSet()
3103+
3104+ # added
3105+ for k in ck.difference(pk):
3106+ delta[k] = Delta(None, mapping[k])
3107+
3108+ # removed
3109+ for k in pk.difference(ck):
3110+ delta[k] = Delta(previous[k], None)
3111+
3112+ # changed
3113+ for k in pk.intersection(ck):
3114+ c = mapping[k]
3115+ p = previous[k]
3116+ if c != p:
3117+ delta[k] = Delta(p, c)
3118+
3119+ return delta
3120+
3121+ @contextlib.contextmanager
3122+ def hook_scope(self, name=""):
3123+ """Scope all future interactions to the current hook execution
3124+ revision."""
3125+ assert not self.revision
3126+ self.cursor.execute(
3127+ 'insert into hooks (hook, date) values (?, ?)',
3128+ (name or sys.argv[0],
3129+ datetime.datetime.utcnow().isoformat()))
3130+ self.revision = self.cursor.lastrowid
3131+ try:
3132+ yield self.revision
3133+ self.revision = None
3134+ except:
3135+ self.flush(False)
3136+ self.revision = None
3137+ raise
3138+ else:
3139+ self.flush()
3140+
3141+ def flush(self, save=True):
3142+ if save:
3143+ self.conn.commit()
3144+ elif self._closed:
3145+ return
3146+ else:
3147+ self.conn.rollback()
3148+
3149+ def _init(self):
3150+ self.cursor.execute('''
3151+ create table if not exists kv (
3152+ key text,
3153+ data text,
3154+ primary key (key)
3155+ )''')
3156+ self.cursor.execute('''
3157+ create table if not exists kv_revisions (
3158+ key text,
3159+ revision integer,
3160+ data text,
3161+ primary key (key, revision)
3162+ )''')
3163+ self.cursor.execute('''
3164+ create table if not exists hooks (
3165+ version integer primary key autoincrement,
3166+ hook text,
3167+ date text
3168+ )''')
3169+ self.conn.commit()
3170+
3171+ def gethistory(self, key, deserialize=False):
3172+ self.cursor.execute(
3173+ '''
3174+ select kv.revision, kv.key, kv.data, h.hook, h.date
3175+ from kv_revisions kv,
3176+ hooks h
3177+ where kv.key=?
3178+ and kv.revision = h.version
3179+ ''', [key])
3180+ if deserialize is False:
3181+ return self.cursor.fetchall()
3182+ return map(_parse_history, self.cursor.fetchall())
3183+
3184+ def debug(self, fh=sys.stderr):
3185+ self.cursor.execute('select * from kv')
3186+ pprint.pprint(self.cursor.fetchall(), stream=fh)
3187+ self.cursor.execute('select * from kv_revisions')
3188+ pprint.pprint(self.cursor.fetchall(), stream=fh)
3189+
3190+
3191+def _parse_history(d):
3192+ return (d[0], d[1], json.loads(d[2]), d[3],
3193+ datetime.datetime.strptime(d[-1], "%Y-%m-%dT%H:%M:%S.%f"))
3194+
3195+
3196+class HookData(object):
3197+ """Simple integration for existing hook exec frameworks.
3198+
3199+ Records all unit information, and stores deltas for processing
3200+ by the hook.
3201+
3202+ Sample::
3203+
3204+ from charmhelper.core import hookenv, unitdata
3205+
3206+ changes = unitdata.HookData()
3207+ db = unitdata.kv()
3208+ hooks = hookenv.Hooks()
3209+
3210+ @hooks.hook
3211+ def config_changed():
3212+ # View all changes to configuration
3213+ for changed, (prev, cur) in changes.conf.items():
3214+ print('config changed', changed,
3215+ 'previous value', prev,
3216+ 'current value', cur)
3217+
3218+ # Get some unit specific bookeeping
3219+ if not db.get('pkg_key'):
3220+ key = urllib.urlopen('https://example.com/pkg_key').read()
3221+ db.set('pkg_key', key)
3222+
3223+ if __name__ == '__main__':
3224+ with changes():
3225+ hook.execute()
3226+
3227+ """
3228+ def __init__(self):
3229+ self.kv = kv()
3230+ self.conf = None
3231+ self.rels = None
3232+
3233+ @contextlib.contextmanager
3234+ def __call__(self):
3235+ from charmhelpers.core import hookenv
3236+ hook_name = hookenv.hook_name()
3237+
3238+ with self.kv.hook_scope(hook_name):
3239+ self._record_charm_version(hookenv.charm_dir())
3240+ delta_config, delta_relation = self._record_hook(hookenv)
3241+ yield self.kv, delta_config, delta_relation
3242+
3243+ def _record_charm_version(self, charm_dir):
3244+ # Record revisions.. charm revisions are meaningless
3245+ # to charm authors as they don't control the revision.
3246+ # so logic dependnent on revision is not particularly
3247+ # useful, however it is useful for debugging analysis.
3248+ charm_rev = open(
3249+ os.path.join(charm_dir, 'revision')).read().strip()
3250+ charm_rev = charm_rev or '0'
3251+ revs = self.kv.get('charm_revisions', [])
3252+ if charm_rev not in revs:
3253+ revs.append(charm_rev.strip() or '0')
3254+ self.kv.set('charm_revisions', revs)
3255+
3256+ def _record_hook(self, hookenv):
3257+ data = hookenv.execution_environment()
3258+ self.conf = conf_delta = self.kv.delta(data['conf'], 'config')
3259+ self.rels = rels_delta = self.kv.delta(data['rels'], 'rels')
3260+ self.kv.set('env', dict(data['env']))
3261+ self.kv.set('unit', data['unit'])
3262+ self.kv.set('relid', data.get('relid'))
3263+ return conf_delta, rels_delta
3264+
3265+
3266+class Record(dict):
3267+
3268+ __slots__ = ()
3269+
3270+ def __getattr__(self, k):
3271+ if k in self:
3272+ return self[k]
3273+ raise AttributeError(k)
3274+
3275+
3276+class DeltaSet(Record):
3277+
3278+ __slots__ = ()
3279+
3280+
3281+Delta = collections.namedtuple('Delta', ['previous', 'current'])
3282+
3283+
3284+_KV = None
3285+
3286+
3287+def kv():
3288+ global _KV
3289+ if _KV is None:
3290+ _KV = Storage()
3291+ return _KV
3292
3293=== added directory 'hooks/charmhelpers/payload'
3294=== added file 'hooks/charmhelpers/payload/__init__.py'
3295--- hooks/charmhelpers/payload/__init__.py 1970-01-01 00:00:00 +0000
3296+++ hooks/charmhelpers/payload/__init__.py 2015-08-30 15:58:19 +0000
3297@@ -0,0 +1,17 @@
3298+# Copyright 2014-2015 Canonical Limited.
3299+#
3300+# This file is part of charm-helpers.
3301+#
3302+# charm-helpers is free software: you can redistribute it and/or modify
3303+# it under the terms of the GNU Lesser General Public License version 3 as
3304+# published by the Free Software Foundation.
3305+#
3306+# charm-helpers is distributed in the hope that it will be useful,
3307+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3308+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3309+# GNU Lesser General Public License for more details.
3310+#
3311+# You should have received a copy of the GNU Lesser General Public License
3312+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
3313+
3314+"Tools for working with files injected into a charm just before deployment."
3315
3316=== added file 'hooks/charmhelpers/payload/execd.py'
3317--- hooks/charmhelpers/payload/execd.py 1970-01-01 00:00:00 +0000
3318+++ hooks/charmhelpers/payload/execd.py 2015-08-30 15:58:19 +0000
3319@@ -0,0 +1,66 @@
3320+#!/usr/bin/env python
3321+
3322+# Copyright 2014-2015 Canonical Limited.
3323+#
3324+# This file is part of charm-helpers.
3325+#
3326+# charm-helpers is free software: you can redistribute it and/or modify
3327+# it under the terms of the GNU Lesser General Public License version 3 as
3328+# published by the Free Software Foundation.
3329+#
3330+# charm-helpers is distributed in the hope that it will be useful,
3331+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3332+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3333+# GNU Lesser General Public License for more details.
3334+#
3335+# You should have received a copy of the GNU Lesser General Public License
3336+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
3337+
3338+import os
3339+import sys
3340+import subprocess
3341+from charmhelpers.core import hookenv
3342+
3343+
3344+def default_execd_dir():
3345+ return os.path.join(os.environ['CHARM_DIR'], 'exec.d')
3346+
3347+
3348+def execd_module_paths(execd_dir=None):
3349+ """Generate a list of full paths to modules within execd_dir."""
3350+ if not execd_dir:
3351+ execd_dir = default_execd_dir()
3352+
3353+ if not os.path.exists(execd_dir):
3354+ return
3355+
3356+ for subpath in os.listdir(execd_dir):
3357+ module = os.path.join(execd_dir, subpath)
3358+ if os.path.isdir(module):
3359+ yield module
3360+
3361+
3362+def execd_submodule_paths(command, execd_dir=None):
3363+ """Generate a list of full paths to the specified command within exec_dir.
3364+ """
3365+ for module_path in execd_module_paths(execd_dir):
3366+ path = os.path.join(module_path, command)
3367+ if os.access(path, os.X_OK) and os.path.isfile(path):
3368+ yield path
3369+
3370+
3371+def execd_run(command, execd_dir=None, die_on_error=False, stderr=None):
3372+ """Run command for each module within execd_dir which defines it."""
3373+ for submodule_path in execd_submodule_paths(command, execd_dir):
3374+ try:
3375+ subprocess.check_call(submodule_path, shell=True, stderr=stderr)
3376+ except subprocess.CalledProcessError as e:
3377+ hookenv.log("Error ({}) running {}. Output: {}".format(
3378+ e.returncode, e.cmd, e.output))
3379+ if die_on_error:
3380+ sys.exit(e.returncode)
3381+
3382+
3383+def execd_preinstall(execd_dir=None):
3384+ """Run charm-pre-install for each module within execd_dir."""
3385+ execd_run('charm-pre-install', execd_dir=execd_dir)
3386
3387=== added file 'hooks/hooks.py'
3388--- hooks/hooks.py 1970-01-01 00:00:00 +0000
3389+++ hooks/hooks.py 2015-08-30 15:58:19 +0000
3390@@ -0,0 +1,31 @@
3391+#!/usr/bin/python
3392+#
3393+# Copyright 2015 Canonical Ltd.
3394+#
3395+# Authors:
3396+# JuanJo Ciarlante <jjo@canonical.com>
3397+#
3398+
3399+import sys
3400+
3401+from charmhelpers.core.hookenv import (
3402+ Hooks,
3403+ UnregisteredHookError,
3404+ log,
3405+)
3406+
3407+from charmhelpers.payload.execd import execd_preinstall
3408+
3409+hooks = Hooks()
3410+
3411+
3412+@hooks.hook('install')
3413+def install():
3414+ execd_preinstall()
3415+
3416+
3417+if __name__ == '__main__':
3418+ try:
3419+ hooks.execute(sys.argv)
3420+ except UnregisteredHookError as e:
3421+ log('Unknown hook {} - skipping.'.format(e))
3422
3423=== added symlink 'hooks/install'
3424=== target is u'hooks.py'

Subscribers

People subscribed via source and target branches

to all changes: