Merge lp:~brad-marshall/charms/trusty/landscape-client/add-nrpe-checks into lp:charms/trusty/landscape-client

Proposed by Brad Marshall
Status: Needs review
Proposed branch: lp:~brad-marshall/charms/trusty/landscape-client/add-nrpe-checks
Merge into: lp:charms/trusty/landscape-client
Diff against target: 2627 lines (+1805/-104)
25 files modified
charm-helpers-sync.yaml (+1/-0)
config.yaml (+16/-0)
hooks/charmhelpers/__init__.py (+38/-0)
hooks/charmhelpers/contrib/__init__.py (+15/-0)
hooks/charmhelpers/contrib/charmsupport/__init__.py (+15/-0)
hooks/charmhelpers/contrib/charmsupport/nrpe.py (+358/-0)
hooks/charmhelpers/contrib/charmsupport/volumes.py (+175/-0)
hooks/charmhelpers/core/__init__.py (+15/-0)
hooks/charmhelpers/core/decorators.py (+57/-0)
hooks/charmhelpers/core/fstab.py (+30/-12)
hooks/charmhelpers/core/hookenv.py (+83/-15)
hooks/charmhelpers/core/host.py (+97/-32)
hooks/charmhelpers/core/services/__init__.py (+18/-2)
hooks/charmhelpers/core/services/base.py (+16/-0)
hooks/charmhelpers/core/services/helpers.py (+37/-9)
hooks/charmhelpers/core/strutils.py (+42/-0)
hooks/charmhelpers/core/sysctl.py (+56/-0)
hooks/charmhelpers/core/templating.py (+20/-3)
hooks/charmhelpers/core/unitdata.py (+477/-0)
hooks/charmhelpers/fetch/__init__.py (+42/-13)
hooks/charmhelpers/fetch/archiveurl.py (+69/-16)
hooks/charmhelpers/fetch/bzrurl.py (+30/-2)
hooks/charmhelpers/fetch/giturl.py (+71/-0)
hooks/hooks.py (+23/-0)
metadata.yaml (+4/-0)
To merge this branch: bzr merge lp:~brad-marshall/charms/trusty/landscape-client/add-nrpe-checks
Reviewer Review Type Date Requested Status
Landscape Pending
Review via email: mp+254693@code.launchpad.net

Description of the change

Add basic NRPE checks to landscape-client charm. Currently just checks the return from the init script, and that at least one landscape-client process is running.

To post a comment you must log in.

Unmerged revisions

47. By Brad Marshall

[bradm] Make the proc check for at least one, rather than exactly one.

46. By Brad Marshall

[bradm] Look for landscape-client as an argument, not a full command

45. By Brad Marshall

[bradm] Moved nrpe config update to before the return in the config-changed hook function

44. By Brad Marshall

[bradm] Import apt_install function from charmhelpers.fetch

43. By Brad Marshall

[bradm] Initial nagios checks, sync charmhelpers

42. By Alberto Donato

Merge from lp:landscape-client-charm.

41. By David Britton

sync merge history from trunk (no file differences) [trivial]

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'charm-helpers-sync.yaml'
2--- charm-helpers-sync.yaml 2014-05-11 12:16:14 +0000
3+++ charm-helpers-sync.yaml 2015-03-31 04:43:29 +0000
4@@ -3,6 +3,7 @@
5 include:
6 - core
7 - fetch
8+ - contrib.charmsupport
9 # - contrib.storage.linux:
10 # - utils
11 # - payload.execd
12
13=== modified file 'config.yaml'
14--- config.yaml 2015-01-22 15:17:28 +0000
15+++ config.yaml 2015-03-31 04:43:29 +0000
16@@ -104,3 +104,19 @@
17 all plugins.
18 type: string
19 default: ALL
20+ nagios_context:
21+ default: "juju"
22+ type: string
23+ description: |
24+ Used by the nrpe-external-master subordinate charm.
25+ A string that will be prepended to instance name to set the host name
26+ in nagios. So for instance the hostname would be something like:
27+ juju-myservice-0
28+ If you're running multiple environments with the same services in them
29+ this allows you to differentiate between them.
30+ nagios_servicegroups:
31+ default: ""
32+ type: string
33+ description: |
34+ A comma-separated list of nagios servicegroups.
35+ If left empty, the nagios_context will be used as the servicegroup
36
37=== modified file 'hooks/charmhelpers/__init__.py'
38--- hooks/charmhelpers/__init__.py 2014-05-12 10:36:48 +0000
39+++ hooks/charmhelpers/__init__.py 2015-03-31 04:43:29 +0000
40@@ -0,0 +1,38 @@
41+# Copyright 2014-2015 Canonical Limited.
42+#
43+# This file is part of charm-helpers.
44+#
45+# charm-helpers is free software: you can redistribute it and/or modify
46+# it under the terms of the GNU Lesser General Public License version 3 as
47+# published by the Free Software Foundation.
48+#
49+# charm-helpers is distributed in the hope that it will be useful,
50+# but WITHOUT ANY WARRANTY; without even the implied warranty of
51+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
52+# GNU Lesser General Public License for more details.
53+#
54+# You should have received a copy of the GNU Lesser General Public License
55+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
56+
57+# Bootstrap charm-helpers, installing its dependencies if necessary using
58+# only standard libraries.
59+import subprocess
60+import sys
61+
62+try:
63+ import six # flake8: noqa
64+except ImportError:
65+ if sys.version_info.major == 2:
66+ subprocess.check_call(['apt-get', 'install', '-y', 'python-six'])
67+ else:
68+ subprocess.check_call(['apt-get', 'install', '-y', 'python3-six'])
69+ import six # flake8: noqa
70+
71+try:
72+ import yaml # flake8: noqa
73+except ImportError:
74+ if sys.version_info.major == 2:
75+ subprocess.check_call(['apt-get', 'install', '-y', 'python-yaml'])
76+ else:
77+ subprocess.check_call(['apt-get', 'install', '-y', 'python3-yaml'])
78+ import yaml # flake8: noqa
79
80=== added directory 'hooks/charmhelpers/contrib'
81=== added file 'hooks/charmhelpers/contrib/__init__.py'
82--- hooks/charmhelpers/contrib/__init__.py 1970-01-01 00:00:00 +0000
83+++ hooks/charmhelpers/contrib/__init__.py 2015-03-31 04:43:29 +0000
84@@ -0,0 +1,15 @@
85+# Copyright 2014-2015 Canonical Limited.
86+#
87+# This file is part of charm-helpers.
88+#
89+# charm-helpers is free software: you can redistribute it and/or modify
90+# it under the terms of the GNU Lesser General Public License version 3 as
91+# published by the Free Software Foundation.
92+#
93+# charm-helpers is distributed in the hope that it will be useful,
94+# but WITHOUT ANY WARRANTY; without even the implied warranty of
95+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
96+# GNU Lesser General Public License for more details.
97+#
98+# You should have received a copy of the GNU Lesser General Public License
99+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
100
101=== added directory 'hooks/charmhelpers/contrib/charmsupport'
102=== added file 'hooks/charmhelpers/contrib/charmsupport/__init__.py'
103--- hooks/charmhelpers/contrib/charmsupport/__init__.py 1970-01-01 00:00:00 +0000
104+++ hooks/charmhelpers/contrib/charmsupport/__init__.py 2015-03-31 04:43:29 +0000
105@@ -0,0 +1,15 @@
106+# Copyright 2014-2015 Canonical Limited.
107+#
108+# This file is part of charm-helpers.
109+#
110+# charm-helpers is free software: you can redistribute it and/or modify
111+# it under the terms of the GNU Lesser General Public License version 3 as
112+# published by the Free Software Foundation.
113+#
114+# charm-helpers is distributed in the hope that it will be useful,
115+# but WITHOUT ANY WARRANTY; without even the implied warranty of
116+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
117+# GNU Lesser General Public License for more details.
118+#
119+# You should have received a copy of the GNU Lesser General Public License
120+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
121
122=== added file 'hooks/charmhelpers/contrib/charmsupport/nrpe.py'
123--- hooks/charmhelpers/contrib/charmsupport/nrpe.py 1970-01-01 00:00:00 +0000
124+++ hooks/charmhelpers/contrib/charmsupport/nrpe.py 2015-03-31 04:43:29 +0000
125@@ -0,0 +1,358 @@
126+# Copyright 2014-2015 Canonical Limited.
127+#
128+# This file is part of charm-helpers.
129+#
130+# charm-helpers is free software: you can redistribute it and/or modify
131+# it under the terms of the GNU Lesser General Public License version 3 as
132+# published by the Free Software Foundation.
133+#
134+# charm-helpers is distributed in the hope that it will be useful,
135+# but WITHOUT ANY WARRANTY; without even the implied warranty of
136+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
137+# GNU Lesser General Public License for more details.
138+#
139+# You should have received a copy of the GNU Lesser General Public License
140+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
141+
142+"""Compatibility with the nrpe-external-master charm"""
143+# Copyright 2012 Canonical Ltd.
144+#
145+# Authors:
146+# Matthew Wedgwood <matthew.wedgwood@canonical.com>
147+
148+import subprocess
149+import pwd
150+import grp
151+import os
152+import glob
153+import shutil
154+import re
155+import shlex
156+import yaml
157+
158+from charmhelpers.core.hookenv import (
159+ config,
160+ local_unit,
161+ log,
162+ relation_ids,
163+ relation_set,
164+ relations_of_type,
165+)
166+
167+from charmhelpers.core.host import service
168+
169+# This module adds compatibility with the nrpe-external-master and plain nrpe
170+# subordinate charms. To use it in your charm:
171+#
172+# 1. Update metadata.yaml
173+#
174+# provides:
175+# (...)
176+# nrpe-external-master:
177+# interface: nrpe-external-master
178+# scope: container
179+#
180+# and/or
181+#
182+# provides:
183+# (...)
184+# local-monitors:
185+# interface: local-monitors
186+# scope: container
187+
188+#
189+# 2. Add the following to config.yaml
190+#
191+# nagios_context:
192+# default: "juju"
193+# type: string
194+# description: |
195+# Used by the nrpe subordinate charms.
196+# A string that will be prepended to instance name to set the host name
197+# in nagios. So for instance the hostname would be something like:
198+# juju-myservice-0
199+# If you're running multiple environments with the same services in them
200+# this allows you to differentiate between them.
201+# nagios_servicegroups:
202+# default: ""
203+# type: string
204+# description: |
205+# A comma-separated list of nagios servicegroups.
206+# If left empty, the nagios_context will be used as the servicegroup
207+#
208+# 3. Add custom checks (Nagios plugins) to files/nrpe-external-master
209+#
210+# 4. Update your hooks.py with something like this:
211+#
212+# from charmsupport.nrpe import NRPE
213+# (...)
214+# def update_nrpe_config():
215+# nrpe_compat = NRPE()
216+# nrpe_compat.add_check(
217+# shortname = "myservice",
218+# description = "Check MyService",
219+# check_cmd = "check_http -w 2 -c 10 http://localhost"
220+# )
221+# nrpe_compat.add_check(
222+# "myservice_other",
223+# "Check for widget failures",
224+# check_cmd = "/srv/myapp/scripts/widget_check"
225+# )
226+# nrpe_compat.write()
227+#
228+# def config_changed():
229+# (...)
230+# update_nrpe_config()
231+#
232+# def nrpe_external_master_relation_changed():
233+# update_nrpe_config()
234+#
235+# def local_monitors_relation_changed():
236+# update_nrpe_config()
237+#
238+# 5. ln -s hooks.py nrpe-external-master-relation-changed
239+# ln -s hooks.py local-monitors-relation-changed
240+
241+
242+class CheckException(Exception):
243+ pass
244+
245+
246+class Check(object):
247+ shortname_re = '[A-Za-z0-9-_]+$'
248+ service_template = ("""
249+#---------------------------------------------------
250+# This file is Juju managed
251+#---------------------------------------------------
252+define service {{
253+ use active-service
254+ host_name {nagios_hostname}
255+ service_description {nagios_hostname}[{shortname}] """
256+ """{description}
257+ check_command check_nrpe!{command}
258+ servicegroups {nagios_servicegroup}
259+}}
260+""")
261+
262+ def __init__(self, shortname, description, check_cmd):
263+ super(Check, self).__init__()
264+ # XXX: could be better to calculate this from the service name
265+ if not re.match(self.shortname_re, shortname):
266+ raise CheckException("shortname must match {}".format(
267+ Check.shortname_re))
268+ self.shortname = shortname
269+ self.command = "check_{}".format(shortname)
270+ # Note: a set of invalid characters is defined by the
271+ # Nagios server config
272+ # The default is: illegal_object_name_chars=`~!$%^&*"|'<>?,()=
273+ self.description = description
274+ self.check_cmd = self._locate_cmd(check_cmd)
275+
276+ def _locate_cmd(self, check_cmd):
277+ search_path = (
278+ '/usr/lib/nagios/plugins',
279+ '/usr/local/lib/nagios/plugins',
280+ )
281+ parts = shlex.split(check_cmd)
282+ for path in search_path:
283+ if os.path.exists(os.path.join(path, parts[0])):
284+ command = os.path.join(path, parts[0])
285+ if len(parts) > 1:
286+ command += " " + " ".join(parts[1:])
287+ return command
288+ log('Check command not found: {}'.format(parts[0]))
289+ return ''
290+
291+ def write(self, nagios_context, hostname, nagios_servicegroups):
292+ nrpe_check_file = '/etc/nagios/nrpe.d/{}.cfg'.format(
293+ self.command)
294+ with open(nrpe_check_file, 'w') as nrpe_check_config:
295+ nrpe_check_config.write("# check {}\n".format(self.shortname))
296+ nrpe_check_config.write("command[{}]={}\n".format(
297+ self.command, self.check_cmd))
298+
299+ if not os.path.exists(NRPE.nagios_exportdir):
300+ log('Not writing service config as {} is not accessible'.format(
301+ NRPE.nagios_exportdir))
302+ else:
303+ self.write_service_config(nagios_context, hostname,
304+ nagios_servicegroups)
305+
306+ def write_service_config(self, nagios_context, hostname,
307+ nagios_servicegroups):
308+ for f in os.listdir(NRPE.nagios_exportdir):
309+ if re.search('.*{}.cfg'.format(self.command), f):
310+ os.remove(os.path.join(NRPE.nagios_exportdir, f))
311+
312+ templ_vars = {
313+ 'nagios_hostname': hostname,
314+ 'nagios_servicegroup': nagios_servicegroups,
315+ 'description': self.description,
316+ 'shortname': self.shortname,
317+ 'command': self.command,
318+ }
319+ nrpe_service_text = Check.service_template.format(**templ_vars)
320+ nrpe_service_file = '{}/service__{}_{}.cfg'.format(
321+ NRPE.nagios_exportdir, hostname, self.command)
322+ with open(nrpe_service_file, 'w') as nrpe_service_config:
323+ nrpe_service_config.write(str(nrpe_service_text))
324+
325+ def run(self):
326+ subprocess.call(self.check_cmd)
327+
328+
329+class NRPE(object):
330+ nagios_logdir = '/var/log/nagios'
331+ nagios_exportdir = '/var/lib/nagios/export'
332+ nrpe_confdir = '/etc/nagios/nrpe.d'
333+
334+ def __init__(self, hostname=None):
335+ super(NRPE, self).__init__()
336+ self.config = config()
337+ self.nagios_context = self.config['nagios_context']
338+ if 'nagios_servicegroups' in self.config and self.config['nagios_servicegroups']:
339+ self.nagios_servicegroups = self.config['nagios_servicegroups']
340+ else:
341+ self.nagios_servicegroups = self.nagios_context
342+ self.unit_name = local_unit().replace('/', '-')
343+ if hostname:
344+ self.hostname = hostname
345+ else:
346+ self.hostname = "{}-{}".format(self.nagios_context, self.unit_name)
347+ self.checks = []
348+
349+ def add_check(self, *args, **kwargs):
350+ self.checks.append(Check(*args, **kwargs))
351+
352+ def write(self):
353+ try:
354+ nagios_uid = pwd.getpwnam('nagios').pw_uid
355+ nagios_gid = grp.getgrnam('nagios').gr_gid
356+ except:
357+ log("Nagios user not set up, nrpe checks not updated")
358+ return
359+
360+ if not os.path.exists(NRPE.nagios_logdir):
361+ os.mkdir(NRPE.nagios_logdir)
362+ os.chown(NRPE.nagios_logdir, nagios_uid, nagios_gid)
363+
364+ nrpe_monitors = {}
365+ monitors = {"monitors": {"remote": {"nrpe": nrpe_monitors}}}
366+ for nrpecheck in self.checks:
367+ nrpecheck.write(self.nagios_context, self.hostname,
368+ self.nagios_servicegroups)
369+ nrpe_monitors[nrpecheck.shortname] = {
370+ "command": nrpecheck.command,
371+ }
372+
373+ service('restart', 'nagios-nrpe-server')
374+
375+ for rid in relation_ids("local-monitors"):
376+ relation_set(relation_id=rid, monitors=yaml.dump(monitors))
377+
378+
379+def get_nagios_hostcontext(relation_name='nrpe-external-master'):
380+ """
381+ Query relation with nrpe subordinate, return the nagios_host_context
382+
383+ :param str relation_name: Name of relation nrpe sub joined to
384+ """
385+ for rel in relations_of_type(relation_name):
386+ if 'nagios_hostname' in rel:
387+ return rel['nagios_host_context']
388+
389+
390+def get_nagios_hostname(relation_name='nrpe-external-master'):
391+ """
392+ Query relation with nrpe subordinate, return the nagios_hostname
393+
394+ :param str relation_name: Name of relation nrpe sub joined to
395+ """
396+ for rel in relations_of_type(relation_name):
397+ if 'nagios_hostname' in rel:
398+ return rel['nagios_hostname']
399+
400+
401+def get_nagios_unit_name(relation_name='nrpe-external-master'):
402+ """
403+ Return the nagios unit name prepended with host_context if needed
404+
405+ :param str relation_name: Name of relation nrpe sub joined to
406+ """
407+ host_context = get_nagios_hostcontext(relation_name)
408+ if host_context:
409+ unit = "%s:%s" % (host_context, local_unit())
410+ else:
411+ unit = local_unit()
412+ return unit
413+
414+
415+def add_init_service_checks(nrpe, services, unit_name):
416+ """
417+ Add checks for each service in list
418+
419+ :param NRPE nrpe: NRPE object to add check to
420+ :param list services: List of services to check
421+ :param str unit_name: Unit name to use in check description
422+ """
423+ for svc in services:
424+ upstart_init = '/etc/init/%s.conf' % svc
425+ sysv_init = '/etc/init.d/%s' % svc
426+ if os.path.exists(upstart_init):
427+ nrpe.add_check(
428+ shortname=svc,
429+ description='process check {%s}' % unit_name,
430+ check_cmd='check_upstart_job %s' % svc
431+ )
432+ elif os.path.exists(sysv_init):
433+ cronpath = '/etc/cron.d/nagios-service-check-%s' % svc
434+ cron_file = ('*/5 * * * * root '
435+ '/usr/local/lib/nagios/plugins/check_exit_status.pl '
436+ '-s /etc/init.d/%s status > '
437+ '/var/lib/nagios/service-check-%s.txt\n' % (svc,
438+ svc)
439+ )
440+ f = open(cronpath, 'w')
441+ f.write(cron_file)
442+ f.close()
443+ nrpe.add_check(
444+ shortname=svc,
445+ description='process check {%s}' % unit_name,
446+ check_cmd='check_status_file.py -f '
447+ '/var/lib/nagios/service-check-%s.txt' % svc,
448+ )
449+
450+
451+def copy_nrpe_checks():
452+ """
453+ Copy the nrpe checks into place
454+
455+ """
456+ NAGIOS_PLUGINS = '/usr/local/lib/nagios/plugins'
457+ nrpe_files_dir = os.path.join(os.getenv('CHARM_DIR'), 'hooks',
458+ 'charmhelpers', 'contrib', 'openstack',
459+ 'files')
460+
461+ if not os.path.exists(NAGIOS_PLUGINS):
462+ os.makedirs(NAGIOS_PLUGINS)
463+ for fname in glob.glob(os.path.join(nrpe_files_dir, "check_*")):
464+ if os.path.isfile(fname):
465+ shutil.copy2(fname,
466+ os.path.join(NAGIOS_PLUGINS, os.path.basename(fname)))
467+
468+
469+def add_haproxy_checks(nrpe, unit_name):
470+ """
471+ Add checks for each service in list
472+
473+ :param NRPE nrpe: NRPE object to add check to
474+ :param str unit_name: Unit name to use in check description
475+ """
476+ nrpe.add_check(
477+ shortname='haproxy_servers',
478+ description='Check HAProxy {%s}' % unit_name,
479+ check_cmd='check_haproxy.sh')
480+ nrpe.add_check(
481+ shortname='haproxy_queue',
482+ description='Check HAProxy queue depth {%s}' % unit_name,
483+ check_cmd='check_haproxy_queue_depth.sh')
484
485=== added file 'hooks/charmhelpers/contrib/charmsupport/volumes.py'
486--- hooks/charmhelpers/contrib/charmsupport/volumes.py 1970-01-01 00:00:00 +0000
487+++ hooks/charmhelpers/contrib/charmsupport/volumes.py 2015-03-31 04:43:29 +0000
488@@ -0,0 +1,175 @@
489+# Copyright 2014-2015 Canonical Limited.
490+#
491+# This file is part of charm-helpers.
492+#
493+# charm-helpers is free software: you can redistribute it and/or modify
494+# it under the terms of the GNU Lesser General Public License version 3 as
495+# published by the Free Software Foundation.
496+#
497+# charm-helpers is distributed in the hope that it will be useful,
498+# but WITHOUT ANY WARRANTY; without even the implied warranty of
499+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
500+# GNU Lesser General Public License for more details.
501+#
502+# You should have received a copy of the GNU Lesser General Public License
503+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
504+
505+'''
506+Functions for managing volumes in juju units. One volume is supported per unit.
507+Subordinates may have their own storage, provided it is on its own partition.
508+
509+Configuration stanzas::
510+
511+ volume-ephemeral:
512+ type: boolean
513+ default: true
514+ description: >
515+ If false, a volume is mounted as sepecified in "volume-map"
516+ If true, ephemeral storage will be used, meaning that log data
517+ will only exist as long as the machine. YOU HAVE BEEN WARNED.
518+ volume-map:
519+ type: string
520+ default: {}
521+ description: >
522+ YAML map of units to device names, e.g:
523+ "{ rsyslog/0: /dev/vdb, rsyslog/1: /dev/vdb }"
524+ Service units will raise a configure-error if volume-ephemeral
525+ is 'true' and no volume-map value is set. Use 'juju set' to set a
526+ value and 'juju resolved' to complete configuration.
527+
528+Usage::
529+
530+ from charmsupport.volumes import configure_volume, VolumeConfigurationError
531+ from charmsupport.hookenv import log, ERROR
532+ def post_mount_hook():
533+ stop_service('myservice')
534+ def post_mount_hook():
535+ start_service('myservice')
536+
537+ if __name__ == '__main__':
538+ try:
539+ configure_volume(before_change=pre_mount_hook,
540+ after_change=post_mount_hook)
541+ except VolumeConfigurationError:
542+ log('Storage could not be configured', ERROR)
543+
544+'''
545+
546+# XXX: Known limitations
547+# - fstab is neither consulted nor updated
548+
549+import os
550+from charmhelpers.core import hookenv
551+from charmhelpers.core import host
552+import yaml
553+
554+
555+MOUNT_BASE = '/srv/juju/volumes'
556+
557+
558+class VolumeConfigurationError(Exception):
559+ '''Volume configuration data is missing or invalid'''
560+ pass
561+
562+
563+def get_config():
564+ '''Gather and sanity-check volume configuration data'''
565+ volume_config = {}
566+ config = hookenv.config()
567+
568+ errors = False
569+
570+ if config.get('volume-ephemeral') in (True, 'True', 'true', 'Yes', 'yes'):
571+ volume_config['ephemeral'] = True
572+ else:
573+ volume_config['ephemeral'] = False
574+
575+ try:
576+ volume_map = yaml.safe_load(config.get('volume-map', '{}'))
577+ except yaml.YAMLError as e:
578+ hookenv.log("Error parsing YAML volume-map: {}".format(e),
579+ hookenv.ERROR)
580+ errors = True
581+ if volume_map is None:
582+ # probably an empty string
583+ volume_map = {}
584+ elif not isinstance(volume_map, dict):
585+ hookenv.log("Volume-map should be a dictionary, not {}".format(
586+ type(volume_map)))
587+ errors = True
588+
589+ volume_config['device'] = volume_map.get(os.environ['JUJU_UNIT_NAME'])
590+ if volume_config['device'] and volume_config['ephemeral']:
591+ # asked for ephemeral storage but also defined a volume ID
592+ hookenv.log('A volume is defined for this unit, but ephemeral '
593+ 'storage was requested', hookenv.ERROR)
594+ errors = True
595+ elif not volume_config['device'] and not volume_config['ephemeral']:
596+ # asked for permanent storage but did not define volume ID
597+ hookenv.log('Ephemeral storage was requested, but there is no volume '
598+ 'defined for this unit.', hookenv.ERROR)
599+ errors = True
600+
601+ unit_mount_name = hookenv.local_unit().replace('/', '-')
602+ volume_config['mountpoint'] = os.path.join(MOUNT_BASE, unit_mount_name)
603+
604+ if errors:
605+ return None
606+ return volume_config
607+
608+
609+def mount_volume(config):
610+ if os.path.exists(config['mountpoint']):
611+ if not os.path.isdir(config['mountpoint']):
612+ hookenv.log('Not a directory: {}'.format(config['mountpoint']))
613+ raise VolumeConfigurationError()
614+ else:
615+ host.mkdir(config['mountpoint'])
616+ if os.path.ismount(config['mountpoint']):
617+ unmount_volume(config)
618+ if not host.mount(config['device'], config['mountpoint'], persist=True):
619+ raise VolumeConfigurationError()
620+
621+
622+def unmount_volume(config):
623+ if os.path.ismount(config['mountpoint']):
624+ if not host.umount(config['mountpoint'], persist=True):
625+ raise VolumeConfigurationError()
626+
627+
628+def managed_mounts():
629+ '''List of all mounted managed volumes'''
630+ return filter(lambda mount: mount[0].startswith(MOUNT_BASE), host.mounts())
631+
632+
633+def configure_volume(before_change=lambda: None, after_change=lambda: None):
634+ '''Set up storage (or don't) according to the charm's volume configuration.
635+ Returns the mount point or "ephemeral". before_change and after_change
636+ are optional functions to be called if the volume configuration changes.
637+ '''
638+
639+ config = get_config()
640+ if not config:
641+ hookenv.log('Failed to read volume configuration', hookenv.CRITICAL)
642+ raise VolumeConfigurationError()
643+
644+ if config['ephemeral']:
645+ if os.path.ismount(config['mountpoint']):
646+ before_change()
647+ unmount_volume(config)
648+ after_change()
649+ return 'ephemeral'
650+ else:
651+ # persistent storage
652+ if os.path.ismount(config['mountpoint']):
653+ mounts = dict(managed_mounts())
654+ if mounts.get(config['mountpoint']) != config['device']:
655+ before_change()
656+ unmount_volume(config)
657+ mount_volume(config)
658+ after_change()
659+ else:
660+ before_change()
661+ mount_volume(config)
662+ after_change()
663+ return config['mountpoint']
664
665=== modified file 'hooks/charmhelpers/core/__init__.py'
666--- hooks/charmhelpers/core/__init__.py 2014-05-12 10:36:48 +0000
667+++ hooks/charmhelpers/core/__init__.py 2015-03-31 04:43:29 +0000
668@@ -0,0 +1,15 @@
669+# Copyright 2014-2015 Canonical Limited.
670+#
671+# This file is part of charm-helpers.
672+#
673+# charm-helpers is free software: you can redistribute it and/or modify
674+# it under the terms of the GNU Lesser General Public License version 3 as
675+# published by the Free Software Foundation.
676+#
677+# charm-helpers is distributed in the hope that it will be useful,
678+# but WITHOUT ANY WARRANTY; without even the implied warranty of
679+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
680+# GNU Lesser General Public License for more details.
681+#
682+# You should have received a copy of the GNU Lesser General Public License
683+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
684
685=== added file 'hooks/charmhelpers/core/decorators.py'
686--- hooks/charmhelpers/core/decorators.py 1970-01-01 00:00:00 +0000
687+++ hooks/charmhelpers/core/decorators.py 2015-03-31 04:43:29 +0000
688@@ -0,0 +1,57 @@
689+# Copyright 2014-2015 Canonical Limited.
690+#
691+# This file is part of charm-helpers.
692+#
693+# charm-helpers is free software: you can redistribute it and/or modify
694+# it under the terms of the GNU Lesser General Public License version 3 as
695+# published by the Free Software Foundation.
696+#
697+# charm-helpers is distributed in the hope that it will be useful,
698+# but WITHOUT ANY WARRANTY; without even the implied warranty of
699+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
700+# GNU Lesser General Public License for more details.
701+#
702+# You should have received a copy of the GNU Lesser General Public License
703+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
704+
705+#
706+# Copyright 2014 Canonical Ltd.
707+#
708+# Authors:
709+# Edward Hope-Morley <opentastic@gmail.com>
710+#
711+
712+import time
713+
714+from charmhelpers.core.hookenv import (
715+ log,
716+ INFO,
717+)
718+
719+
720+def retry_on_exception(num_retries, base_delay=0, exc_type=Exception):
721+ """If the decorated function raises exception exc_type, allow num_retries
722+ retry attempts before raise the exception.
723+ """
724+ def _retry_on_exception_inner_1(f):
725+ def _retry_on_exception_inner_2(*args, **kwargs):
726+ retries = num_retries
727+ multiplier = 1
728+ while True:
729+ try:
730+ return f(*args, **kwargs)
731+ except exc_type:
732+ if not retries:
733+ raise
734+
735+ delay = base_delay * multiplier
736+ multiplier += 1
737+ log("Retrying '%s' %d more times (delay=%s)" %
738+ (f.__name__, retries, delay), level=INFO)
739+ retries -= 1
740+ if delay:
741+ time.sleep(delay)
742+
743+ return _retry_on_exception_inner_2
744+
745+ return _retry_on_exception_inner_1
746
747=== modified file 'hooks/charmhelpers/core/fstab.py'
748--- hooks/charmhelpers/core/fstab.py 2014-09-26 08:54:54 +0000
749+++ hooks/charmhelpers/core/fstab.py 2015-03-31 04:43:29 +0000
750@@ -1,12 +1,29 @@
751 #!/usr/bin/env python
752 # -*- coding: utf-8 -*-
753
754+# Copyright 2014-2015 Canonical Limited.
755+#
756+# This file is part of charm-helpers.
757+#
758+# charm-helpers is free software: you can redistribute it and/or modify
759+# it under the terms of the GNU Lesser General Public License version 3 as
760+# published by the Free Software Foundation.
761+#
762+# charm-helpers is distributed in the hope that it will be useful,
763+# but WITHOUT ANY WARRANTY; without even the implied warranty of
764+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
765+# GNU Lesser General Public License for more details.
766+#
767+# You should have received a copy of the GNU Lesser General Public License
768+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
769+
770+import io
771+import os
772+
773 __author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
774
775-import os
776-
777-
778-class Fstab(file):
779+
780+class Fstab(io.FileIO):
781 """This class extends file in order to implement a file reader/writer
782 for file `/etc/fstab`
783 """
784@@ -24,8 +41,8 @@
785 options = "defaults"
786
787 self.options = options
788- self.d = d
789- self.p = p
790+ self.d = int(d)
791+ self.p = int(p)
792
793 def __eq__(self, o):
794 return str(self) == str(o)
795@@ -45,7 +62,7 @@
796 self._path = path
797 else:
798 self._path = self.DEFAULT_PATH
799- file.__init__(self, self._path, 'r+')
800+ super(Fstab, self).__init__(self._path, 'rb+')
801
802 def _hydrate_entry(self, line):
803 # NOTE: use split with no arguments to split on any
804@@ -58,8 +75,9 @@
805 def entries(self):
806 self.seek(0)
807 for line in self.readlines():
808+ line = line.decode('us-ascii')
809 try:
810- if not line.startswith("#"):
811+ if line.strip() and not line.strip().startswith("#"):
812 yield self._hydrate_entry(line)
813 except ValueError:
814 pass
815@@ -75,18 +93,18 @@
816 if self.get_entry_by_attr('device', entry.device):
817 return False
818
819- self.write(str(entry) + '\n')
820+ self.write((str(entry) + '\n').encode('us-ascii'))
821 self.truncate()
822 return entry
823
824 def remove_entry(self, entry):
825 self.seek(0)
826
827- lines = self.readlines()
828+ lines = [l.decode('us-ascii') for l in self.readlines()]
829
830 found = False
831 for index, line in enumerate(lines):
832- if not line.startswith("#"):
833+ if line.strip() and not line.strip().startswith("#"):
834 if self._hydrate_entry(line) == entry:
835 found = True
836 break
837@@ -97,7 +115,7 @@
838 lines.remove(line)
839
840 self.seek(0)
841- self.write(''.join(lines))
842+ self.write(''.join(lines).encode('us-ascii'))
843 self.truncate()
844 return True
845
846
847=== modified file 'hooks/charmhelpers/core/hookenv.py'
848--- hooks/charmhelpers/core/hookenv.py 2014-09-26 08:54:54 +0000
849+++ hooks/charmhelpers/core/hookenv.py 2015-03-31 04:43:29 +0000
850@@ -1,3 +1,19 @@
851+# Copyright 2014-2015 Canonical Limited.
852+#
853+# This file is part of charm-helpers.
854+#
855+# charm-helpers is free software: you can redistribute it and/or modify
856+# it under the terms of the GNU Lesser General Public License version 3 as
857+# published by the Free Software Foundation.
858+#
859+# charm-helpers is distributed in the hope that it will be useful,
860+# but WITHOUT ANY WARRANTY; without even the implied warranty of
861+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
862+# GNU Lesser General Public License for more details.
863+#
864+# You should have received a copy of the GNU Lesser General Public License
865+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
866+
867 "Interactions with the Juju environment"
868 # Copyright 2013 Canonical Ltd.
869 #
870@@ -9,9 +25,14 @@
871 import yaml
872 import subprocess
873 import sys
874-import UserDict
875 from subprocess import CalledProcessError
876
877+import six
878+if not six.PY3:
879+ from UserDict import UserDict
880+else:
881+ from collections import UserDict
882+
883 CRITICAL = "CRITICAL"
884 ERROR = "ERROR"
885 WARNING = "WARNING"
886@@ -63,16 +84,18 @@
887 command = ['juju-log']
888 if level:
889 command += ['-l', level]
890+ if not isinstance(message, six.string_types):
891+ message = repr(message)
892 command += [message]
893 subprocess.call(command)
894
895
896-class Serializable(UserDict.IterableUserDict):
897+class Serializable(UserDict):
898 """Wrapper, an object that can be serialized to yaml or json"""
899
900 def __init__(self, obj):
901 # wrap the object
902- UserDict.IterableUserDict.__init__(self)
903+ UserDict.__init__(self)
904 self.data = obj
905
906 def __getattr__(self, attr):
907@@ -214,6 +237,12 @@
908 except KeyError:
909 return (self._prev_dict or {})[key]
910
911+ def keys(self):
912+ prev_keys = []
913+ if self._prev_dict is not None:
914+ prev_keys = self._prev_dict.keys()
915+ return list(set(prev_keys + list(dict.keys(self))))
916+
917 def load_previous(self, path=None):
918 """Load previous copy of config from disk.
919
920@@ -263,7 +292,7 @@
921
922 """
923 if self._prev_dict:
924- for k, v in self._prev_dict.iteritems():
925+ for k, v in six.iteritems(self._prev_dict):
926 if k not in self:
927 self[k] = v
928 with open(self.path, 'w') as f:
929@@ -278,7 +307,8 @@
930 config_cmd_line.append(scope)
931 config_cmd_line.append('--format=json')
932 try:
933- config_data = json.loads(subprocess.check_output(config_cmd_line))
934+ config_data = json.loads(
935+ subprocess.check_output(config_cmd_line).decode('UTF-8'))
936 if scope is not None:
937 return config_data
938 return Config(config_data)
939@@ -297,10 +327,10 @@
940 if unit:
941 _args.append(unit)
942 try:
943- return json.loads(subprocess.check_output(_args))
944+ return json.loads(subprocess.check_output(_args).decode('UTF-8'))
945 except ValueError:
946 return None
947- except CalledProcessError, e:
948+ except CalledProcessError as e:
949 if e.returncode == 2:
950 return None
951 raise
952@@ -312,7 +342,7 @@
953 relation_cmd_line = ['relation-set']
954 if relation_id is not None:
955 relation_cmd_line.extend(('-r', relation_id))
956- for k, v in (relation_settings.items() + kwargs.items()):
957+ for k, v in (list(relation_settings.items()) + list(kwargs.items())):
958 if v is None:
959 relation_cmd_line.append('{}='.format(k))
960 else:
961@@ -329,7 +359,8 @@
962 relid_cmd_line = ['relation-ids', '--format=json']
963 if reltype is not None:
964 relid_cmd_line.append(reltype)
965- return json.loads(subprocess.check_output(relid_cmd_line)) or []
966+ return json.loads(
967+ subprocess.check_output(relid_cmd_line).decode('UTF-8')) or []
968 return []
969
970
971@@ -340,7 +371,8 @@
972 units_cmd_line = ['relation-list', '--format=json']
973 if relid is not None:
974 units_cmd_line.extend(('-r', relid))
975- return json.loads(subprocess.check_output(units_cmd_line)) or []
976+ return json.loads(
977+ subprocess.check_output(units_cmd_line).decode('UTF-8')) or []
978
979
980 @cached
981@@ -380,21 +412,31 @@
982
983
984 @cached
985+def metadata():
986+ """Get the current charm metadata.yaml contents as a python object"""
987+ with open(os.path.join(charm_dir(), 'metadata.yaml')) as md:
988+ return yaml.safe_load(md)
989+
990+
991+@cached
992 def relation_types():
993 """Get a list of relation types supported by this charm"""
994- charmdir = os.environ.get('CHARM_DIR', '')
995- mdf = open(os.path.join(charmdir, 'metadata.yaml'))
996- md = yaml.safe_load(mdf)
997 rel_types = []
998+ md = metadata()
999 for key in ('provides', 'requires', 'peers'):
1000 section = md.get(key)
1001 if section:
1002 rel_types.extend(section.keys())
1003- mdf.close()
1004 return rel_types
1005
1006
1007 @cached
1008+def charm_name():
1009+ """Get the name of the current charm as is specified on metadata.yaml"""
1010+ return metadata().get('name')
1011+
1012+
1013+@cached
1014 def relations():
1015 """Get a nested dictionary of relation data for all related units"""
1016 rels = {}
1017@@ -449,7 +491,7 @@
1018 """Get the unit ID for the remote unit"""
1019 _args = ['unit-get', '--format=json', attribute]
1020 try:
1021- return json.loads(subprocess.check_output(_args))
1022+ return json.loads(subprocess.check_output(_args).decode('UTF-8'))
1023 except ValueError:
1024 return None
1025
1026@@ -524,3 +566,29 @@
1027 def charm_dir():
1028 """Return the root directory of the current charm"""
1029 return os.environ.get('CHARM_DIR')
1030+
1031+
1032+@cached
1033+def action_get(key=None):
1034+ """Gets the value of an action parameter, or all key/value param pairs"""
1035+ cmd = ['action-get']
1036+ if key is not None:
1037+ cmd.append(key)
1038+ cmd.append('--format=json')
1039+ action_data = json.loads(subprocess.check_output(cmd).decode('UTF-8'))
1040+ return action_data
1041+
1042+
1043+def action_set(values):
1044+ """Sets the values to be returned after the action finishes"""
1045+ cmd = ['action-set']
1046+ for k, v in list(values.items()):
1047+ cmd.append('{}={}'.format(k, v))
1048+ subprocess.check_call(cmd)
1049+
1050+
1051+def action_fail(message):
1052+ """Sets the action status to failed and sets the error message.
1053+
1054+ The results set by action_set are preserved."""
1055+ subprocess.check_call(['action-fail', message])
1056
1057=== modified file 'hooks/charmhelpers/core/host.py'
1058--- hooks/charmhelpers/core/host.py 2014-09-26 08:54:54 +0000
1059+++ hooks/charmhelpers/core/host.py 2015-03-31 04:43:29 +0000
1060@@ -1,3 +1,19 @@
1061+# Copyright 2014-2015 Canonical Limited.
1062+#
1063+# This file is part of charm-helpers.
1064+#
1065+# charm-helpers is free software: you can redistribute it and/or modify
1066+# it under the terms of the GNU Lesser General Public License version 3 as
1067+# published by the Free Software Foundation.
1068+#
1069+# charm-helpers is distributed in the hope that it will be useful,
1070+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1071+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1072+# GNU Lesser General Public License for more details.
1073+#
1074+# You should have received a copy of the GNU Lesser General Public License
1075+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1076+
1077 """Tools for working with the host system"""
1078 # Copyright 2012 Canonical Ltd.
1079 #
1080@@ -6,19 +22,20 @@
1081 # Matthew Wedgwood <matthew.wedgwood@canonical.com>
1082
1083 import os
1084+import re
1085 import pwd
1086 import grp
1087 import random
1088 import string
1089 import subprocess
1090 import hashlib
1091-import shutil
1092 from contextlib import contextmanager
1093-
1094 from collections import OrderedDict
1095
1096-from hookenv import log
1097-from fstab import Fstab
1098+import six
1099+
1100+from .hookenv import log
1101+from .fstab import Fstab
1102
1103
1104 def service_start(service_name):
1105@@ -54,7 +71,9 @@
1106 def service_running(service):
1107 """Determine whether a system service is running"""
1108 try:
1109- output = subprocess.check_output(['service', service, 'status'], stderr=subprocess.STDOUT)
1110+ output = subprocess.check_output(
1111+ ['service', service, 'status'],
1112+ stderr=subprocess.STDOUT).decode('UTF-8')
1113 except subprocess.CalledProcessError:
1114 return False
1115 else:
1116@@ -67,7 +86,9 @@
1117 def service_available(service_name):
1118 """Determine whether a system service is available"""
1119 try:
1120- subprocess.check_output(['service', service_name, 'status'], stderr=subprocess.STDOUT)
1121+ subprocess.check_output(
1122+ ['service', service_name, 'status'],
1123+ stderr=subprocess.STDOUT).decode('UTF-8')
1124 except subprocess.CalledProcessError as e:
1125 return 'unrecognized service' not in e.output
1126 else:
1127@@ -96,6 +117,26 @@
1128 return user_info
1129
1130
1131+def add_group(group_name, system_group=False):
1132+ """Add a group to the system"""
1133+ try:
1134+ group_info = grp.getgrnam(group_name)
1135+ log('group {0} already exists!'.format(group_name))
1136+ except KeyError:
1137+ log('creating group {0}'.format(group_name))
1138+ cmd = ['addgroup']
1139+ if system_group:
1140+ cmd.append('--system')
1141+ else:
1142+ cmd.extend([
1143+ '--group',
1144+ ])
1145+ cmd.append(group_name)
1146+ subprocess.check_call(cmd)
1147+ group_info = grp.getgrnam(group_name)
1148+ return group_info
1149+
1150+
1151 def add_user_to_group(username, group):
1152 """Add a user to a group"""
1153 cmd = [
1154@@ -115,7 +156,7 @@
1155 cmd.append(from_path)
1156 cmd.append(to_path)
1157 log(" ".join(cmd))
1158- return subprocess.check_output(cmd).strip()
1159+ return subprocess.check_output(cmd).decode('UTF-8').strip()
1160
1161
1162 def symlink(source, destination):
1163@@ -130,28 +171,31 @@
1164 subprocess.check_call(cmd)
1165
1166
1167-def mkdir(path, owner='root', group='root', perms=0555, force=False):
1168+def mkdir(path, owner='root', group='root', perms=0o555, force=False):
1169 """Create a directory"""
1170 log("Making dir {} {}:{} {:o}".format(path, owner, group,
1171 perms))
1172 uid = pwd.getpwnam(owner).pw_uid
1173 gid = grp.getgrnam(group).gr_gid
1174 realpath = os.path.abspath(path)
1175- if os.path.exists(realpath):
1176- if force and not os.path.isdir(realpath):
1177+ path_exists = os.path.exists(realpath)
1178+ if path_exists and force:
1179+ if not os.path.isdir(realpath):
1180 log("Removing non-directory file {} prior to mkdir()".format(path))
1181 os.unlink(realpath)
1182- else:
1183+ os.makedirs(realpath, perms)
1184+ elif not path_exists:
1185 os.makedirs(realpath, perms)
1186 os.chown(realpath, uid, gid)
1187-
1188-
1189-def write_file(path, content, owner='root', group='root', perms=0444):
1190- """Create or overwrite a file with the contents of a string"""
1191+ os.chmod(realpath, perms)
1192+
1193+
1194+def write_file(path, content, owner='root', group='root', perms=0o444):
1195+ """Create or overwrite a file with the contents of a byte string."""
1196 log("Writing file {} {}:{} {:o}".format(path, owner, group, perms))
1197 uid = pwd.getpwnam(owner).pw_uid
1198 gid = grp.getgrnam(group).gr_gid
1199- with open(path, 'w') as target:
1200+ with open(path, 'wb') as target:
1201 os.fchown(target.fileno(), uid, gid)
1202 os.fchmod(target.fileno(), perms)
1203 target.write(content)
1204@@ -177,7 +221,7 @@
1205 cmd_args.extend([device, mountpoint])
1206 try:
1207 subprocess.check_output(cmd_args)
1208- except subprocess.CalledProcessError, e:
1209+ except subprocess.CalledProcessError as e:
1210 log('Error mounting {} at {}\n{}'.format(device, mountpoint, e.output))
1211 return False
1212
1213@@ -191,7 +235,7 @@
1214 cmd_args = ['umount', mountpoint]
1215 try:
1216 subprocess.check_output(cmd_args)
1217- except subprocess.CalledProcessError, e:
1218+ except subprocess.CalledProcessError as e:
1219 log('Error unmounting {}\n{}'.format(mountpoint, e.output))
1220 return False
1221
1222@@ -218,8 +262,8 @@
1223 """
1224 if os.path.exists(path):
1225 h = getattr(hashlib, hash_type)()
1226- with open(path, 'r') as source:
1227- h.update(source.read()) # IGNORE:E1101 - it does have update
1228+ with open(path, 'rb') as source:
1229+ h.update(source.read())
1230 return h.hexdigest()
1231 else:
1232 return None
1233@@ -261,11 +305,11 @@
1234 ceph_client_changed function.
1235 """
1236 def wrap(f):
1237- def wrapped_f(*args):
1238+ def wrapped_f(*args, **kwargs):
1239 checksums = {}
1240 for path in restart_map:
1241 checksums[path] = file_hash(path)
1242- f(*args)
1243+ f(*args, **kwargs)
1244 restarts = []
1245 for path in restart_map:
1246 if checksums[path] != file_hash(path):
1247@@ -295,29 +339,39 @@
1248 def pwgen(length=None):
1249 """Generate a random pasword."""
1250 if length is None:
1251+ # A random length is ok to use a weak PRNG
1252 length = random.choice(range(35, 45))
1253 alphanumeric_chars = [
1254- l for l in (string.letters + string.digits)
1255+ l for l in (string.ascii_letters + string.digits)
1256 if l not in 'l0QD1vAEIOUaeiou']
1257+ # Use a crypto-friendly PRNG (e.g. /dev/urandom) for making the
1258+ # actual password
1259+ random_generator = random.SystemRandom()
1260 random_chars = [
1261- random.choice(alphanumeric_chars) for _ in range(length)]
1262+ random_generator.choice(alphanumeric_chars) for _ in range(length)]
1263 return(''.join(random_chars))
1264
1265
1266 def list_nics(nic_type):
1267 '''Return a list of nics of given type(s)'''
1268- if isinstance(nic_type, basestring):
1269+ if isinstance(nic_type, six.string_types):
1270 int_types = [nic_type]
1271 else:
1272 int_types = nic_type
1273 interfaces = []
1274 for int_type in int_types:
1275 cmd = ['ip', 'addr', 'show', 'label', int_type + '*']
1276- ip_output = subprocess.check_output(cmd).split('\n')
1277+ ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
1278 ip_output = (line for line in ip_output if line)
1279 for line in ip_output:
1280 if line.split()[1].startswith(int_type):
1281- interfaces.append(line.split()[1].replace(":", ""))
1282+ matched = re.search('.*: (' + int_type + r'[0-9]+\.[0-9]+)@.*', line)
1283+ if matched:
1284+ interface = matched.groups()[0]
1285+ else:
1286+ interface = line.split()[1].replace(":", "")
1287+ interfaces.append(interface)
1288+
1289 return interfaces
1290
1291
1292@@ -329,7 +383,7 @@
1293
1294 def get_nic_mtu(nic):
1295 cmd = ['ip', 'addr', 'show', nic]
1296- ip_output = subprocess.check_output(cmd).split('\n')
1297+ ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
1298 mtu = ""
1299 for line in ip_output:
1300 words = line.split()
1301@@ -340,7 +394,7 @@
1302
1303 def get_nic_hwaddr(nic):
1304 cmd = ['ip', '-o', '-0', 'addr', 'show', nic]
1305- ip_output = subprocess.check_output(cmd)
1306+ ip_output = subprocess.check_output(cmd).decode('UTF-8')
1307 hwaddr = ""
1308 words = ip_output.split()
1309 if 'link/ether' in words:
1310@@ -355,10 +409,13 @@
1311 * 0 => Installed revno is the same as supplied arg
1312 * -1 => Installed revno is less than supplied arg
1313
1314+ This function imports apt_cache function from charmhelpers.fetch if
1315+ the pkgcache argument is None. Be sure to add charmhelpers.fetch if
1316+ you call this function, or pass an apt_pkg.Cache() instance.
1317 '''
1318 import apt_pkg
1319- from charmhelpers.fetch import apt_cache
1320 if not pkgcache:
1321+ from charmhelpers.fetch import apt_cache
1322 pkgcache = apt_cache()
1323 pkg = pkgcache[package]
1324 return apt_pkg.version_compare(pkg.current_ver.ver_str, revno)
1325@@ -373,13 +430,21 @@
1326 os.chdir(cur)
1327
1328
1329-def chownr(path, owner, group):
1330+def chownr(path, owner, group, follow_links=True):
1331 uid = pwd.getpwnam(owner).pw_uid
1332 gid = grp.getgrnam(group).gr_gid
1333+ if follow_links:
1334+ chown = os.chown
1335+ else:
1336+ chown = os.lchown
1337
1338 for root, dirs, files in os.walk(path):
1339 for name in dirs + files:
1340 full = os.path.join(root, name)
1341 broken_symlink = os.path.lexists(full) and not os.path.exists(full)
1342 if not broken_symlink:
1343- os.chown(full, uid, gid)
1344+ chown(full, uid, gid)
1345+
1346+
1347+def lchownr(path, owner, group):
1348+ chownr(path, owner, group, follow_links=False)
1349
1350=== modified file 'hooks/charmhelpers/core/services/__init__.py'
1351--- hooks/charmhelpers/core/services/__init__.py 2014-09-26 08:54:54 +0000
1352+++ hooks/charmhelpers/core/services/__init__.py 2015-03-31 04:43:29 +0000
1353@@ -1,2 +1,18 @@
1354-from .base import *
1355-from .helpers import *
1356+# Copyright 2014-2015 Canonical Limited.
1357+#
1358+# This file is part of charm-helpers.
1359+#
1360+# charm-helpers is free software: you can redistribute it and/or modify
1361+# it under the terms of the GNU Lesser General Public License version 3 as
1362+# published by the Free Software Foundation.
1363+#
1364+# charm-helpers is distributed in the hope that it will be useful,
1365+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1366+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1367+# GNU Lesser General Public License for more details.
1368+#
1369+# You should have received a copy of the GNU Lesser General Public License
1370+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1371+
1372+from .base import * # NOQA
1373+from .helpers import * # NOQA
1374
1375=== modified file 'hooks/charmhelpers/core/services/base.py'
1376--- hooks/charmhelpers/core/services/base.py 2014-09-26 08:54:54 +0000
1377+++ hooks/charmhelpers/core/services/base.py 2015-03-31 04:43:29 +0000
1378@@ -1,3 +1,19 @@
1379+# Copyright 2014-2015 Canonical Limited.
1380+#
1381+# This file is part of charm-helpers.
1382+#
1383+# charm-helpers is free software: you can redistribute it and/or modify
1384+# it under the terms of the GNU Lesser General Public License version 3 as
1385+# published by the Free Software Foundation.
1386+#
1387+# charm-helpers is distributed in the hope that it will be useful,
1388+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1389+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1390+# GNU Lesser General Public License for more details.
1391+#
1392+# You should have received a copy of the GNU Lesser General Public License
1393+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1394+
1395 import os
1396 import re
1397 import json
1398
1399=== modified file 'hooks/charmhelpers/core/services/helpers.py'
1400--- hooks/charmhelpers/core/services/helpers.py 2014-09-26 08:54:54 +0000
1401+++ hooks/charmhelpers/core/services/helpers.py 2015-03-31 04:43:29 +0000
1402@@ -1,3 +1,19 @@
1403+# Copyright 2014-2015 Canonical Limited.
1404+#
1405+# This file is part of charm-helpers.
1406+#
1407+# charm-helpers is free software: you can redistribute it and/or modify
1408+# it under the terms of the GNU Lesser General Public License version 3 as
1409+# published by the Free Software Foundation.
1410+#
1411+# charm-helpers is distributed in the hope that it will be useful,
1412+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1413+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1414+# GNU Lesser General Public License for more details.
1415+#
1416+# You should have received a copy of the GNU Lesser General Public License
1417+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1418+
1419 import os
1420 import yaml
1421 from charmhelpers.core import hookenv
1422@@ -29,12 +45,14 @@
1423 """
1424 name = None
1425 interface = None
1426- required_keys = []
1427
1428 def __init__(self, name=None, additional_required_keys=None):
1429+ if not hasattr(self, 'required_keys'):
1430+ self.required_keys = []
1431+
1432 if name is not None:
1433 self.name = name
1434- if additional_required_keys is not None:
1435+ if additional_required_keys:
1436 self.required_keys.extend(additional_required_keys)
1437 self.get_data()
1438
1439@@ -118,7 +136,10 @@
1440 """
1441 name = 'db'
1442 interface = 'mysql'
1443- required_keys = ['host', 'user', 'password', 'database']
1444+
1445+ def __init__(self, *args, **kwargs):
1446+ self.required_keys = ['host', 'user', 'password', 'database']
1447+ super(HttpRelation).__init__(self, *args, **kwargs)
1448
1449
1450 class HttpRelation(RelationContext):
1451@@ -130,7 +151,10 @@
1452 """
1453 name = 'website'
1454 interface = 'http'
1455- required_keys = ['host', 'port']
1456+
1457+ def __init__(self, *args, **kwargs):
1458+ self.required_keys = ['host', 'port']
1459+ super(HttpRelation).__init__(self, *args, **kwargs)
1460
1461 def provide_data(self):
1462 return {
1463@@ -196,7 +220,7 @@
1464 if not os.path.isabs(file_name):
1465 file_name = os.path.join(hookenv.charm_dir(), file_name)
1466 with open(file_name, 'w') as file_stream:
1467- os.fchmod(file_stream.fileno(), 0600)
1468+ os.fchmod(file_stream.fileno(), 0o600)
1469 yaml.dump(config_data, file_stream)
1470
1471 def read_context(self, file_name):
1472@@ -211,15 +235,19 @@
1473
1474 class TemplateCallback(ManagerCallback):
1475 """
1476- Callback class that will render a Jinja2 template, for use as a ready action.
1477-
1478- :param str source: The template source file, relative to `$CHARM_DIR/templates`
1479+ Callback class that will render a Jinja2 template, for use as a ready
1480+ action.
1481+
1482+ :param str source: The template source file, relative to
1483+ `$CHARM_DIR/templates`
1484+
1485 :param str target: The target to write the rendered template to
1486 :param str owner: The owner of the rendered file
1487 :param str group: The group of the rendered file
1488 :param int perms: The permissions of the rendered file
1489 """
1490- def __init__(self, source, target, owner='root', group='root', perms=0444):
1491+ def __init__(self, source, target,
1492+ owner='root', group='root', perms=0o444):
1493 self.source = source
1494 self.target = target
1495 self.owner = owner
1496
1497=== added file 'hooks/charmhelpers/core/strutils.py'
1498--- hooks/charmhelpers/core/strutils.py 1970-01-01 00:00:00 +0000
1499+++ hooks/charmhelpers/core/strutils.py 2015-03-31 04:43:29 +0000
1500@@ -0,0 +1,42 @@
1501+#!/usr/bin/env python
1502+# -*- coding: utf-8 -*-
1503+
1504+# Copyright 2014-2015 Canonical Limited.
1505+#
1506+# This file is part of charm-helpers.
1507+#
1508+# charm-helpers is free software: you can redistribute it and/or modify
1509+# it under the terms of the GNU Lesser General Public License version 3 as
1510+# published by the Free Software Foundation.
1511+#
1512+# charm-helpers is distributed in the hope that it will be useful,
1513+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1514+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1515+# GNU Lesser General Public License for more details.
1516+#
1517+# You should have received a copy of the GNU Lesser General Public License
1518+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1519+
1520+import six
1521+
1522+
1523+def bool_from_string(value):
1524+ """Interpret string value as boolean.
1525+
1526+ Returns True if value translates to True otherwise False.
1527+ """
1528+ if isinstance(value, six.string_types):
1529+ value = six.text_type(value)
1530+ else:
1531+ msg = "Unable to interpret non-string value '%s' as boolean" % (value)
1532+ raise ValueError(msg)
1533+
1534+ value = value.strip().lower()
1535+
1536+ if value in ['y', 'yes', 'true', 't']:
1537+ return True
1538+ elif value in ['n', 'no', 'false', 'f']:
1539+ return False
1540+
1541+ msg = "Unable to interpret string value '%s' as boolean" % (value)
1542+ raise ValueError(msg)
1543
1544=== added file 'hooks/charmhelpers/core/sysctl.py'
1545--- hooks/charmhelpers/core/sysctl.py 1970-01-01 00:00:00 +0000
1546+++ hooks/charmhelpers/core/sysctl.py 2015-03-31 04:43:29 +0000
1547@@ -0,0 +1,56 @@
1548+#!/usr/bin/env python
1549+# -*- coding: utf-8 -*-
1550+
1551+# Copyright 2014-2015 Canonical Limited.
1552+#
1553+# This file is part of charm-helpers.
1554+#
1555+# charm-helpers is free software: you can redistribute it and/or modify
1556+# it under the terms of the GNU Lesser General Public License version 3 as
1557+# published by the Free Software Foundation.
1558+#
1559+# charm-helpers is distributed in the hope that it will be useful,
1560+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1561+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1562+# GNU Lesser General Public License for more details.
1563+#
1564+# You should have received a copy of the GNU Lesser General Public License
1565+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1566+
1567+import yaml
1568+
1569+from subprocess import check_call
1570+
1571+from charmhelpers.core.hookenv import (
1572+ log,
1573+ DEBUG,
1574+ ERROR,
1575+)
1576+
1577+__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
1578+
1579+
1580+def create(sysctl_dict, sysctl_file):
1581+ """Creates a sysctl.conf file from a YAML associative array
1582+
1583+ :param sysctl_dict: a YAML-formatted string of sysctl options eg "{ 'kernel.max_pid': 1337 }"
1584+ :type sysctl_dict: str
1585+ :param sysctl_file: path to the sysctl file to be saved
1586+ :type sysctl_file: str or unicode
1587+ :returns: None
1588+ """
1589+ try:
1590+ sysctl_dict_parsed = yaml.safe_load(sysctl_dict)
1591+ except yaml.YAMLError:
1592+ log("Error parsing YAML sysctl_dict: {}".format(sysctl_dict),
1593+ level=ERROR)
1594+ return
1595+
1596+ with open(sysctl_file, "w") as fd:
1597+ for key, value in sysctl_dict_parsed.items():
1598+ fd.write("{}={}\n".format(key, value))
1599+
1600+ log("Updating sysctl_file: %s values: %s" % (sysctl_file, sysctl_dict_parsed),
1601+ level=DEBUG)
1602+
1603+ check_call(["sysctl", "-p", sysctl_file])
1604
1605=== modified file 'hooks/charmhelpers/core/templating.py'
1606--- hooks/charmhelpers/core/templating.py 2014-09-26 08:54:54 +0000
1607+++ hooks/charmhelpers/core/templating.py 2015-03-31 04:43:29 +0000
1608@@ -1,10 +1,27 @@
1609+# Copyright 2014-2015 Canonical Limited.
1610+#
1611+# This file is part of charm-helpers.
1612+#
1613+# charm-helpers is free software: you can redistribute it and/or modify
1614+# it under the terms of the GNU Lesser General Public License version 3 as
1615+# published by the Free Software Foundation.
1616+#
1617+# charm-helpers is distributed in the hope that it will be useful,
1618+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1619+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1620+# GNU Lesser General Public License for more details.
1621+#
1622+# You should have received a copy of the GNU Lesser General Public License
1623+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1624+
1625 import os
1626
1627 from charmhelpers.core import host
1628 from charmhelpers.core import hookenv
1629
1630
1631-def render(source, target, context, owner='root', group='root', perms=0444, templates_dir=None):
1632+def render(source, target, context, owner='root', group='root',
1633+ perms=0o444, templates_dir=None, encoding='UTF-8'):
1634 """
1635 Render a template.
1636
1637@@ -47,5 +64,5 @@
1638 level=hookenv.ERROR)
1639 raise e
1640 content = template.render(context)
1641- host.mkdir(os.path.dirname(target))
1642- host.write_file(target, content, owner, group, perms)
1643+ host.mkdir(os.path.dirname(target), owner, group, perms=0o755)
1644+ host.write_file(target, content.encode(encoding), owner, group, perms)
1645
1646=== added file 'hooks/charmhelpers/core/unitdata.py'
1647--- hooks/charmhelpers/core/unitdata.py 1970-01-01 00:00:00 +0000
1648+++ hooks/charmhelpers/core/unitdata.py 2015-03-31 04:43:29 +0000
1649@@ -0,0 +1,477 @@
1650+#!/usr/bin/env python
1651+# -*- coding: utf-8 -*-
1652+#
1653+# Copyright 2014-2015 Canonical Limited.
1654+#
1655+# This file is part of charm-helpers.
1656+#
1657+# charm-helpers is free software: you can redistribute it and/or modify
1658+# it under the terms of the GNU Lesser General Public License version 3 as
1659+# published by the Free Software Foundation.
1660+#
1661+# charm-helpers is distributed in the hope that it will be useful,
1662+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1663+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1664+# GNU Lesser General Public License for more details.
1665+#
1666+# You should have received a copy of the GNU Lesser General Public License
1667+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1668+#
1669+#
1670+# Authors:
1671+# Kapil Thangavelu <kapil.foss@gmail.com>
1672+#
1673+"""
1674+Intro
1675+-----
1676+
1677+A simple way to store state in units. This provides a key value
1678+storage with support for versioned, transactional operation,
1679+and can calculate deltas from previous values to simplify unit logic
1680+when processing changes.
1681+
1682+
1683+Hook Integration
1684+----------------
1685+
1686+There are several extant frameworks for hook execution, including
1687+
1688+ - charmhelpers.core.hookenv.Hooks
1689+ - charmhelpers.core.services.ServiceManager
1690+
1691+The storage classes are framework agnostic, one simple integration is
1692+via the HookData contextmanager. It will record the current hook
1693+execution environment (including relation data, config data, etc.),
1694+setup a transaction and allow easy access to the changes from
1695+previously seen values. One consequence of the integration is the
1696+reservation of particular keys ('rels', 'unit', 'env', 'config',
1697+'charm_revisions') for their respective values.
1698+
1699+Here's a fully worked integration example using hookenv.Hooks::
1700+
1701+ from charmhelper.core import hookenv, unitdata
1702+
1703+ hook_data = unitdata.HookData()
1704+ db = unitdata.kv()
1705+ hooks = hookenv.Hooks()
1706+
1707+ @hooks.hook
1708+ def config_changed():
1709+ # Print all changes to configuration from previously seen
1710+ # values.
1711+ for changed, (prev, cur) in hook_data.conf.items():
1712+ print('config changed', changed,
1713+ 'previous value', prev,
1714+ 'current value', cur)
1715+
1716+ # Get some unit specific bookeeping
1717+ if not db.get('pkg_key'):
1718+ key = urllib.urlopen('https://example.com/pkg_key').read()
1719+ db.set('pkg_key', key)
1720+
1721+ # Directly access all charm config as a mapping.
1722+ conf = db.getrange('config', True)
1723+
1724+ # Directly access all relation data as a mapping
1725+ rels = db.getrange('rels', True)
1726+
1727+ if __name__ == '__main__':
1728+ with hook_data():
1729+ hook.execute()
1730+
1731+
1732+A more basic integration is via the hook_scope context manager which simply
1733+manages transaction scope (and records hook name, and timestamp)::
1734+
1735+ >>> from unitdata import kv
1736+ >>> db = kv()
1737+ >>> with db.hook_scope('install'):
1738+ ... # do work, in transactional scope.
1739+ ... db.set('x', 1)
1740+ >>> db.get('x')
1741+ 1
1742+
1743+
1744+Usage
1745+-----
1746+
1747+Values are automatically json de/serialized to preserve basic typing
1748+and complex data struct capabilities (dicts, lists, ints, booleans, etc).
1749+
1750+Individual values can be manipulated via get/set::
1751+
1752+ >>> kv.set('y', True)
1753+ >>> kv.get('y')
1754+ True
1755+
1756+ # We can set complex values (dicts, lists) as a single key.
1757+ >>> kv.set('config', {'a': 1, 'b': True'})
1758+
1759+ # Also supports returning dictionaries as a record which
1760+ # provides attribute access.
1761+ >>> config = kv.get('config', record=True)
1762+ >>> config.b
1763+ True
1764+
1765+
1766+Groups of keys can be manipulated with update/getrange::
1767+
1768+ >>> kv.update({'z': 1, 'y': 2}, prefix="gui.")
1769+ >>> kv.getrange('gui.', strip=True)
1770+ {'z': 1, 'y': 2}
1771+
1772+When updating values, its very helpful to understand which values
1773+have actually changed and how have they changed. The storage
1774+provides a delta method to provide for this::
1775+
1776+ >>> data = {'debug': True, 'option': 2}
1777+ >>> delta = kv.delta(data, 'config.')
1778+ >>> delta.debug.previous
1779+ None
1780+ >>> delta.debug.current
1781+ True
1782+ >>> delta
1783+ {'debug': (None, True), 'option': (None, 2)}
1784+
1785+Note the delta method does not persist the actual change, it needs to
1786+be explicitly saved via 'update' method::
1787+
1788+ >>> kv.update(data, 'config.')
1789+
1790+Values modified in the context of a hook scope retain historical values
1791+associated to the hookname.
1792+
1793+ >>> with db.hook_scope('config-changed'):
1794+ ... db.set('x', 42)
1795+ >>> db.gethistory('x')
1796+ [(1, u'x', 1, u'install', u'2015-01-21T16:49:30.038372'),
1797+ (2, u'x', 42, u'config-changed', u'2015-01-21T16:49:30.038786')]
1798+
1799+"""
1800+
1801+import collections
1802+import contextlib
1803+import datetime
1804+import json
1805+import os
1806+import pprint
1807+import sqlite3
1808+import sys
1809+
1810+__author__ = 'Kapil Thangavelu <kapil.foss@gmail.com>'
1811+
1812+
1813+class Storage(object):
1814+ """Simple key value database for local unit state within charms.
1815+
1816+ Modifications are automatically committed at hook exit. That's
1817+ currently regardless of exit code.
1818+
1819+ To support dicts, lists, integer, floats, and booleans values
1820+ are automatically json encoded/decoded.
1821+ """
1822+ def __init__(self, path=None):
1823+ self.db_path = path
1824+ if path is None:
1825+ self.db_path = os.path.join(
1826+ os.environ.get('CHARM_DIR', ''), '.unit-state.db')
1827+ self.conn = sqlite3.connect('%s' % self.db_path)
1828+ self.cursor = self.conn.cursor()
1829+ self.revision = None
1830+ self._closed = False
1831+ self._init()
1832+
1833+ def close(self):
1834+ if self._closed:
1835+ return
1836+ self.flush(False)
1837+ self.cursor.close()
1838+ self.conn.close()
1839+ self._closed = True
1840+
1841+ def _scoped_query(self, stmt, params=None):
1842+ if params is None:
1843+ params = []
1844+ return stmt, params
1845+
1846+ def get(self, key, default=None, record=False):
1847+ self.cursor.execute(
1848+ *self._scoped_query(
1849+ 'select data from kv where key=?', [key]))
1850+ result = self.cursor.fetchone()
1851+ if not result:
1852+ return default
1853+ if record:
1854+ return Record(json.loads(result[0]))
1855+ return json.loads(result[0])
1856+
1857+ def getrange(self, key_prefix, strip=False):
1858+ stmt = "select key, data from kv where key like '%s%%'" % key_prefix
1859+ self.cursor.execute(*self._scoped_query(stmt))
1860+ result = self.cursor.fetchall()
1861+
1862+ if not result:
1863+ return None
1864+ if not strip:
1865+ key_prefix = ''
1866+ return dict([
1867+ (k[len(key_prefix):], json.loads(v)) for k, v in result])
1868+
1869+ def update(self, mapping, prefix=""):
1870+ for k, v in mapping.items():
1871+ self.set("%s%s" % (prefix, k), v)
1872+
1873+ def unset(self, key):
1874+ self.cursor.execute('delete from kv where key=?', [key])
1875+ if self.revision and self.cursor.rowcount:
1876+ self.cursor.execute(
1877+ 'insert into kv_revisions values (?, ?, ?)',
1878+ [key, self.revision, json.dumps('DELETED')])
1879+
1880+ def set(self, key, value):
1881+ serialized = json.dumps(value)
1882+
1883+ self.cursor.execute(
1884+ 'select data from kv where key=?', [key])
1885+ exists = self.cursor.fetchone()
1886+
1887+ # Skip mutations to the same value
1888+ if exists:
1889+ if exists[0] == serialized:
1890+ return value
1891+
1892+ if not exists:
1893+ self.cursor.execute(
1894+ 'insert into kv (key, data) values (?, ?)',
1895+ (key, serialized))
1896+ else:
1897+ self.cursor.execute('''
1898+ update kv
1899+ set data = ?
1900+ where key = ?''', [serialized, key])
1901+
1902+ # Save
1903+ if not self.revision:
1904+ return value
1905+
1906+ self.cursor.execute(
1907+ 'select 1 from kv_revisions where key=? and revision=?',
1908+ [key, self.revision])
1909+ exists = self.cursor.fetchone()
1910+
1911+ if not exists:
1912+ self.cursor.execute(
1913+ '''insert into kv_revisions (
1914+ revision, key, data) values (?, ?, ?)''',
1915+ (self.revision, key, serialized))
1916+ else:
1917+ self.cursor.execute(
1918+ '''
1919+ update kv_revisions
1920+ set data = ?
1921+ where key = ?
1922+ and revision = ?''',
1923+ [serialized, key, self.revision])
1924+
1925+ return value
1926+
1927+ def delta(self, mapping, prefix):
1928+ """
1929+ return a delta containing values that have changed.
1930+ """
1931+ previous = self.getrange(prefix, strip=True)
1932+ if not previous:
1933+ pk = set()
1934+ else:
1935+ pk = set(previous.keys())
1936+ ck = set(mapping.keys())
1937+ delta = DeltaSet()
1938+
1939+ # added
1940+ for k in ck.difference(pk):
1941+ delta[k] = Delta(None, mapping[k])
1942+
1943+ # removed
1944+ for k in pk.difference(ck):
1945+ delta[k] = Delta(previous[k], None)
1946+
1947+ # changed
1948+ for k in pk.intersection(ck):
1949+ c = mapping[k]
1950+ p = previous[k]
1951+ if c != p:
1952+ delta[k] = Delta(p, c)
1953+
1954+ return delta
1955+
1956+ @contextlib.contextmanager
1957+ def hook_scope(self, name=""):
1958+ """Scope all future interactions to the current hook execution
1959+ revision."""
1960+ assert not self.revision
1961+ self.cursor.execute(
1962+ 'insert into hooks (hook, date) values (?, ?)',
1963+ (name or sys.argv[0],
1964+ datetime.datetime.utcnow().isoformat()))
1965+ self.revision = self.cursor.lastrowid
1966+ try:
1967+ yield self.revision
1968+ self.revision = None
1969+ except:
1970+ self.flush(False)
1971+ self.revision = None
1972+ raise
1973+ else:
1974+ self.flush()
1975+
1976+ def flush(self, save=True):
1977+ if save:
1978+ self.conn.commit()
1979+ elif self._closed:
1980+ return
1981+ else:
1982+ self.conn.rollback()
1983+
1984+ def _init(self):
1985+ self.cursor.execute('''
1986+ create table if not exists kv (
1987+ key text,
1988+ data text,
1989+ primary key (key)
1990+ )''')
1991+ self.cursor.execute('''
1992+ create table if not exists kv_revisions (
1993+ key text,
1994+ revision integer,
1995+ data text,
1996+ primary key (key, revision)
1997+ )''')
1998+ self.cursor.execute('''
1999+ create table if not exists hooks (
2000+ version integer primary key autoincrement,
2001+ hook text,
2002+ date text
2003+ )''')
2004+ self.conn.commit()
2005+
2006+ def gethistory(self, key, deserialize=False):
2007+ self.cursor.execute(
2008+ '''
2009+ select kv.revision, kv.key, kv.data, h.hook, h.date
2010+ from kv_revisions kv,
2011+ hooks h
2012+ where kv.key=?
2013+ and kv.revision = h.version
2014+ ''', [key])
2015+ if deserialize is False:
2016+ return self.cursor.fetchall()
2017+ return map(_parse_history, self.cursor.fetchall())
2018+
2019+ def debug(self, fh=sys.stderr):
2020+ self.cursor.execute('select * from kv')
2021+ pprint.pprint(self.cursor.fetchall(), stream=fh)
2022+ self.cursor.execute('select * from kv_revisions')
2023+ pprint.pprint(self.cursor.fetchall(), stream=fh)
2024+
2025+
2026+def _parse_history(d):
2027+ return (d[0], d[1], json.loads(d[2]), d[3],
2028+ datetime.datetime.strptime(d[-1], "%Y-%m-%dT%H:%M:%S.%f"))
2029+
2030+
2031+class HookData(object):
2032+ """Simple integration for existing hook exec frameworks.
2033+
2034+ Records all unit information, and stores deltas for processing
2035+ by the hook.
2036+
2037+ Sample::
2038+
2039+ from charmhelper.core import hookenv, unitdata
2040+
2041+ changes = unitdata.HookData()
2042+ db = unitdata.kv()
2043+ hooks = hookenv.Hooks()
2044+
2045+ @hooks.hook
2046+ def config_changed():
2047+ # View all changes to configuration
2048+ for changed, (prev, cur) in changes.conf.items():
2049+ print('config changed', changed,
2050+ 'previous value', prev,
2051+ 'current value', cur)
2052+
2053+ # Get some unit specific bookeeping
2054+ if not db.get('pkg_key'):
2055+ key = urllib.urlopen('https://example.com/pkg_key').read()
2056+ db.set('pkg_key', key)
2057+
2058+ if __name__ == '__main__':
2059+ with changes():
2060+ hook.execute()
2061+
2062+ """
2063+ def __init__(self):
2064+ self.kv = kv()
2065+ self.conf = None
2066+ self.rels = None
2067+
2068+ @contextlib.contextmanager
2069+ def __call__(self):
2070+ from charmhelpers.core import hookenv
2071+ hook_name = hookenv.hook_name()
2072+
2073+ with self.kv.hook_scope(hook_name):
2074+ self._record_charm_version(hookenv.charm_dir())
2075+ delta_config, delta_relation = self._record_hook(hookenv)
2076+ yield self.kv, delta_config, delta_relation
2077+
2078+ def _record_charm_version(self, charm_dir):
2079+ # Record revisions.. charm revisions are meaningless
2080+ # to charm authors as they don't control the revision.
2081+ # so logic dependnent on revision is not particularly
2082+ # useful, however it is useful for debugging analysis.
2083+ charm_rev = open(
2084+ os.path.join(charm_dir, 'revision')).read().strip()
2085+ charm_rev = charm_rev or '0'
2086+ revs = self.kv.get('charm_revisions', [])
2087+ if charm_rev not in revs:
2088+ revs.append(charm_rev.strip() or '0')
2089+ self.kv.set('charm_revisions', revs)
2090+
2091+ def _record_hook(self, hookenv):
2092+ data = hookenv.execution_environment()
2093+ self.conf = conf_delta = self.kv.delta(data['conf'], 'config')
2094+ self.rels = rels_delta = self.kv.delta(data['rels'], 'rels')
2095+ self.kv.set('env', data['env'])
2096+ self.kv.set('unit', data['unit'])
2097+ self.kv.set('relid', data.get('relid'))
2098+ return conf_delta, rels_delta
2099+
2100+
2101+class Record(dict):
2102+
2103+ __slots__ = ()
2104+
2105+ def __getattr__(self, k):
2106+ if k in self:
2107+ return self[k]
2108+ raise AttributeError(k)
2109+
2110+
2111+class DeltaSet(Record):
2112+
2113+ __slots__ = ()
2114+
2115+
2116+Delta = collections.namedtuple('Delta', ['previous', 'current'])
2117+
2118+
2119+_KV = None
2120+
2121+
2122+def kv():
2123+ global _KV
2124+ if _KV is None:
2125+ _KV = Storage()
2126+ return _KV
2127
2128=== modified file 'hooks/charmhelpers/fetch/__init__.py'
2129--- hooks/charmhelpers/fetch/__init__.py 2014-09-26 08:54:54 +0000
2130+++ hooks/charmhelpers/fetch/__init__.py 2015-03-31 04:43:29 +0000
2131@@ -1,3 +1,19 @@
2132+# Copyright 2014-2015 Canonical Limited.
2133+#
2134+# This file is part of charm-helpers.
2135+#
2136+# charm-helpers is free software: you can redistribute it and/or modify
2137+# it under the terms of the GNU Lesser General Public License version 3 as
2138+# published by the Free Software Foundation.
2139+#
2140+# charm-helpers is distributed in the hope that it will be useful,
2141+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2142+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2143+# GNU Lesser General Public License for more details.
2144+#
2145+# You should have received a copy of the GNU Lesser General Public License
2146+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2147+
2148 import importlib
2149 from tempfile import NamedTemporaryFile
2150 import time
2151@@ -5,10 +21,6 @@
2152 from charmhelpers.core.host import (
2153 lsb_release
2154 )
2155-from urlparse import (
2156- urlparse,
2157- urlunparse,
2158-)
2159 import subprocess
2160 from charmhelpers.core.hookenv import (
2161 config,
2162@@ -16,6 +28,12 @@
2163 )
2164 import os
2165
2166+import six
2167+if six.PY3:
2168+ from urllib.parse import urlparse, urlunparse
2169+else:
2170+ from urlparse import urlparse, urlunparse
2171+
2172
2173 CLOUD_ARCHIVE = """# Ubuntu Cloud Archive
2174 deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main
2175@@ -62,9 +80,16 @@
2176 'trusty-juno/updates': 'trusty-updates/juno',
2177 'trusty-updates/juno': 'trusty-updates/juno',
2178 'juno/proposed': 'trusty-proposed/juno',
2179- 'juno/proposed': 'trusty-proposed/juno',
2180 'trusty-juno/proposed': 'trusty-proposed/juno',
2181 'trusty-proposed/juno': 'trusty-proposed/juno',
2182+ # Kilo
2183+ 'kilo': 'trusty-updates/kilo',
2184+ 'trusty-kilo': 'trusty-updates/kilo',
2185+ 'trusty-kilo/updates': 'trusty-updates/kilo',
2186+ 'trusty-updates/kilo': 'trusty-updates/kilo',
2187+ 'kilo/proposed': 'trusty-proposed/kilo',
2188+ 'trusty-kilo/proposed': 'trusty-proposed/kilo',
2189+ 'trusty-proposed/kilo': 'trusty-proposed/kilo',
2190 }
2191
2192 # The order of this list is very important. Handlers should be listed in from
2193@@ -72,6 +97,7 @@
2194 FETCH_HANDLERS = (
2195 'charmhelpers.fetch.archiveurl.ArchiveUrlFetchHandler',
2196 'charmhelpers.fetch.bzrurl.BzrUrlFetchHandler',
2197+ 'charmhelpers.fetch.giturl.GitUrlFetchHandler',
2198 )
2199
2200 APT_NO_LOCK = 100 # The return code for "couldn't acquire lock" in APT.
2201@@ -148,7 +174,7 @@
2202 cmd = ['apt-get', '--assume-yes']
2203 cmd.extend(options)
2204 cmd.append('install')
2205- if isinstance(packages, basestring):
2206+ if isinstance(packages, six.string_types):
2207 cmd.append(packages)
2208 else:
2209 cmd.extend(packages)
2210@@ -181,7 +207,7 @@
2211 def apt_purge(packages, fatal=False):
2212 """Purge one or more packages"""
2213 cmd = ['apt-get', '--assume-yes', 'purge']
2214- if isinstance(packages, basestring):
2215+ if isinstance(packages, six.string_types):
2216 cmd.append(packages)
2217 else:
2218 cmd.extend(packages)
2219@@ -192,7 +218,7 @@
2220 def apt_hold(packages, fatal=False):
2221 """Hold one or more packages"""
2222 cmd = ['apt-mark', 'hold']
2223- if isinstance(packages, basestring):
2224+ if isinstance(packages, six.string_types):
2225 cmd.append(packages)
2226 else:
2227 cmd.extend(packages)
2228@@ -218,6 +244,7 @@
2229 pocket for the release.
2230 'cloud:' may be used to activate official cloud archive pockets,
2231 such as 'cloud:icehouse'
2232+ 'distro' may be used as a noop
2233
2234 @param key: A key to be added to the system's APT keyring and used
2235 to verify the signatures on packages. Ideally, this should be an
2236@@ -251,12 +278,14 @@
2237 release = lsb_release()['DISTRIB_CODENAME']
2238 with open('/etc/apt/sources.list.d/proposed.list', 'w') as apt:
2239 apt.write(PROPOSED_POCKET.format(release))
2240+ elif source == 'distro':
2241+ pass
2242 else:
2243- raise SourceConfigError("Unknown source: {!r}".format(source))
2244+ log("Unknown source: {!r}".format(source))
2245
2246 if key:
2247 if '-----BEGIN PGP PUBLIC KEY BLOCK-----' in key:
2248- with NamedTemporaryFile() as key_file:
2249+ with NamedTemporaryFile('w+') as key_file:
2250 key_file.write(key)
2251 key_file.flush()
2252 key_file.seek(0)
2253@@ -293,14 +322,14 @@
2254 sources = safe_load((config(sources_var) or '').strip()) or []
2255 keys = safe_load((config(keys_var) or '').strip()) or None
2256
2257- if isinstance(sources, basestring):
2258+ if isinstance(sources, six.string_types):
2259 sources = [sources]
2260
2261 if keys is None:
2262 for source in sources:
2263 add_source(source, None)
2264 else:
2265- if isinstance(keys, basestring):
2266+ if isinstance(keys, six.string_types):
2267 keys = [keys]
2268
2269 if len(sources) != len(keys):
2270@@ -397,7 +426,7 @@
2271 while result is None or result == APT_NO_LOCK:
2272 try:
2273 result = subprocess.check_call(cmd, env=env)
2274- except subprocess.CalledProcessError, e:
2275+ except subprocess.CalledProcessError as e:
2276 retry_count = retry_count + 1
2277 if retry_count > APT_NO_LOCK_RETRY_COUNT:
2278 raise
2279
2280=== modified file 'hooks/charmhelpers/fetch/archiveurl.py'
2281--- hooks/charmhelpers/fetch/archiveurl.py 2014-09-26 08:54:54 +0000
2282+++ hooks/charmhelpers/fetch/archiveurl.py 2015-03-31 04:43:29 +0000
2283@@ -1,8 +1,22 @@
2284+# Copyright 2014-2015 Canonical Limited.
2285+#
2286+# This file is part of charm-helpers.
2287+#
2288+# charm-helpers is free software: you can redistribute it and/or modify
2289+# it under the terms of the GNU Lesser General Public License version 3 as
2290+# published by the Free Software Foundation.
2291+#
2292+# charm-helpers is distributed in the hope that it will be useful,
2293+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2294+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2295+# GNU Lesser General Public License for more details.
2296+#
2297+# You should have received a copy of the GNU Lesser General Public License
2298+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2299+
2300 import os
2301-import urllib2
2302-from urllib import urlretrieve
2303-import urlparse
2304 import hashlib
2305+import re
2306
2307 from charmhelpers.fetch import (
2308 BaseFetchHandler,
2309@@ -14,6 +28,41 @@
2310 )
2311 from charmhelpers.core.host import mkdir, check_hash
2312
2313+import six
2314+if six.PY3:
2315+ from urllib.request import (
2316+ build_opener, install_opener, urlopen, urlretrieve,
2317+ HTTPPasswordMgrWithDefaultRealm, HTTPBasicAuthHandler,
2318+ )
2319+ from urllib.parse import urlparse, urlunparse, parse_qs
2320+ from urllib.error import URLError
2321+else:
2322+ from urllib import urlretrieve
2323+ from urllib2 import (
2324+ build_opener, install_opener, urlopen,
2325+ HTTPPasswordMgrWithDefaultRealm, HTTPBasicAuthHandler,
2326+ URLError
2327+ )
2328+ from urlparse import urlparse, urlunparse, parse_qs
2329+
2330+
2331+def splituser(host):
2332+ '''urllib.splituser(), but six's support of this seems broken'''
2333+ _userprog = re.compile('^(.*)@(.*)$')
2334+ match = _userprog.match(host)
2335+ if match:
2336+ return match.group(1, 2)
2337+ return None, host
2338+
2339+
2340+def splitpasswd(user):
2341+ '''urllib.splitpasswd(), but six's support of this is missing'''
2342+ _passwdprog = re.compile('^([^:]*):(.*)$', re.S)
2343+ match = _passwdprog.match(user)
2344+ if match:
2345+ return match.group(1, 2)
2346+ return user, None
2347+
2348
2349 class ArchiveUrlFetchHandler(BaseFetchHandler):
2350 """
2351@@ -42,20 +91,20 @@
2352 """
2353 # propogate all exceptions
2354 # URLError, OSError, etc
2355- proto, netloc, path, params, query, fragment = urlparse.urlparse(source)
2356+ proto, netloc, path, params, query, fragment = urlparse(source)
2357 if proto in ('http', 'https'):
2358- auth, barehost = urllib2.splituser(netloc)
2359+ auth, barehost = splituser(netloc)
2360 if auth is not None:
2361- source = urlparse.urlunparse((proto, barehost, path, params, query, fragment))
2362- username, password = urllib2.splitpasswd(auth)
2363- passman = urllib2.HTTPPasswordMgrWithDefaultRealm()
2364+ source = urlunparse((proto, barehost, path, params, query, fragment))
2365+ username, password = splitpasswd(auth)
2366+ passman = HTTPPasswordMgrWithDefaultRealm()
2367 # Realm is set to None in add_password to force the username and password
2368 # to be used whatever the realm
2369 passman.add_password(None, source, username, password)
2370- authhandler = urllib2.HTTPBasicAuthHandler(passman)
2371- opener = urllib2.build_opener(authhandler)
2372- urllib2.install_opener(opener)
2373- response = urllib2.urlopen(source)
2374+ authhandler = HTTPBasicAuthHandler(passman)
2375+ opener = build_opener(authhandler)
2376+ install_opener(opener)
2377+ response = urlopen(source)
2378 try:
2379 with open(dest, 'w') as dest_file:
2380 dest_file.write(response.read())
2381@@ -91,17 +140,21 @@
2382 url_parts = self.parse_url(source)
2383 dest_dir = os.path.join(os.environ.get('CHARM_DIR'), 'fetched')
2384 if not os.path.exists(dest_dir):
2385- mkdir(dest_dir, perms=0755)
2386+ mkdir(dest_dir, perms=0o755)
2387 dld_file = os.path.join(dest_dir, os.path.basename(url_parts.path))
2388 try:
2389 self.download(source, dld_file)
2390- except urllib2.URLError as e:
2391+ except URLError as e:
2392 raise UnhandledSource(e.reason)
2393 except OSError as e:
2394 raise UnhandledSource(e.strerror)
2395- options = urlparse.parse_qs(url_parts.fragment)
2396+ options = parse_qs(url_parts.fragment)
2397 for key, value in options.items():
2398- if key in hashlib.algorithms:
2399+ if not six.PY3:
2400+ algorithms = hashlib.algorithms
2401+ else:
2402+ algorithms = hashlib.algorithms_available
2403+ if key in algorithms:
2404 check_hash(dld_file, value, key)
2405 if checksum:
2406 check_hash(dld_file, checksum, hash_type)
2407
2408=== modified file 'hooks/charmhelpers/fetch/bzrurl.py'
2409--- hooks/charmhelpers/fetch/bzrurl.py 2014-09-26 08:54:54 +0000
2410+++ hooks/charmhelpers/fetch/bzrurl.py 2015-03-31 04:43:29 +0000
2411@@ -1,3 +1,19 @@
2412+# Copyright 2014-2015 Canonical Limited.
2413+#
2414+# This file is part of charm-helpers.
2415+#
2416+# charm-helpers is free software: you can redistribute it and/or modify
2417+# it under the terms of the GNU Lesser General Public License version 3 as
2418+# published by the Free Software Foundation.
2419+#
2420+# charm-helpers is distributed in the hope that it will be useful,
2421+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2422+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2423+# GNU Lesser General Public License for more details.
2424+#
2425+# You should have received a copy of the GNU Lesser General Public License
2426+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2427+
2428 import os
2429 from charmhelpers.fetch import (
2430 BaseFetchHandler,
2431@@ -5,12 +21,18 @@
2432 )
2433 from charmhelpers.core.host import mkdir
2434
2435+import six
2436+if six.PY3:
2437+ raise ImportError('bzrlib does not support Python3')
2438+
2439 try:
2440 from bzrlib.branch import Branch
2441+ from bzrlib import bzrdir, workingtree, errors
2442 except ImportError:
2443 from charmhelpers.fetch import apt_install
2444 apt_install("python-bzrlib")
2445 from bzrlib.branch import Branch
2446+ from bzrlib import bzrdir, workingtree, errors
2447
2448
2449 class BzrUrlFetchHandler(BaseFetchHandler):
2450@@ -31,8 +53,14 @@
2451 from bzrlib.plugin import load_plugins
2452 load_plugins()
2453 try:
2454+ local_branch = bzrdir.BzrDir.create_branch_convenience(dest)
2455+ except errors.AlreadyControlDirError:
2456+ local_branch = Branch.open(dest)
2457+ try:
2458 remote_branch = Branch.open(source)
2459- remote_branch.bzrdir.sprout(dest).open_branch()
2460+ remote_branch.push(local_branch)
2461+ tree = workingtree.WorkingTree.open(dest)
2462+ tree.update()
2463 except Exception as e:
2464 raise e
2465
2466@@ -42,7 +70,7 @@
2467 dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",
2468 branch_name)
2469 if not os.path.exists(dest_dir):
2470- mkdir(dest_dir, perms=0755)
2471+ mkdir(dest_dir, perms=0o755)
2472 try:
2473 self.branch(source, dest_dir)
2474 except OSError as e:
2475
2476=== added file 'hooks/charmhelpers/fetch/giturl.py'
2477--- hooks/charmhelpers/fetch/giturl.py 1970-01-01 00:00:00 +0000
2478+++ hooks/charmhelpers/fetch/giturl.py 2015-03-31 04:43:29 +0000
2479@@ -0,0 +1,71 @@
2480+# Copyright 2014-2015 Canonical Limited.
2481+#
2482+# This file is part of charm-helpers.
2483+#
2484+# charm-helpers is free software: you can redistribute it and/or modify
2485+# it under the terms of the GNU Lesser General Public License version 3 as
2486+# published by the Free Software Foundation.
2487+#
2488+# charm-helpers is distributed in the hope that it will be useful,
2489+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2490+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2491+# GNU Lesser General Public License for more details.
2492+#
2493+# You should have received a copy of the GNU Lesser General Public License
2494+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2495+
2496+import os
2497+from charmhelpers.fetch import (
2498+ BaseFetchHandler,
2499+ UnhandledSource
2500+)
2501+from charmhelpers.core.host import mkdir
2502+
2503+import six
2504+if six.PY3:
2505+ raise ImportError('GitPython does not support Python 3')
2506+
2507+try:
2508+ from git import Repo
2509+except ImportError:
2510+ from charmhelpers.fetch import apt_install
2511+ apt_install("python-git")
2512+ from git import Repo
2513+
2514+from git.exc import GitCommandError # noqa E402
2515+
2516+
2517+class GitUrlFetchHandler(BaseFetchHandler):
2518+ """Handler for git branches via generic and github URLs"""
2519+ def can_handle(self, source):
2520+ url_parts = self.parse_url(source)
2521+ # TODO (mattyw) no support for ssh git@ yet
2522+ if url_parts.scheme not in ('http', 'https', 'git'):
2523+ return False
2524+ else:
2525+ return True
2526+
2527+ def clone(self, source, dest, branch):
2528+ if not self.can_handle(source):
2529+ raise UnhandledSource("Cannot handle {}".format(source))
2530+
2531+ repo = Repo.clone_from(source, dest)
2532+ repo.git.checkout(branch)
2533+
2534+ def install(self, source, branch="master", dest=None):
2535+ url_parts = self.parse_url(source)
2536+ branch_name = url_parts.path.strip("/").split("/")[-1]
2537+ if dest:
2538+ dest_dir = os.path.join(dest, branch_name)
2539+ else:
2540+ dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",
2541+ branch_name)
2542+ if not os.path.exists(dest_dir):
2543+ mkdir(dest_dir, perms=0o755)
2544+ try:
2545+ self.clone(source, dest_dir, branch)
2546+ except GitCommandError as e:
2547+ raise UnhandledSource(e.message)
2548+ except OSError as e:
2549+ raise UnhandledSource(e.strerror)
2550+ return dest_dir
2551
2552=== modified file 'hooks/hooks.py'
2553--- hooks/hooks.py 2014-09-25 07:00:05 +0000
2554+++ hooks/hooks.py 2015-03-31 04:43:29 +0000
2555@@ -7,9 +7,12 @@
2556
2557 from charmhelpers.core.hookenv import (
2558 Hooks, UnregisteredHookError, log)
2559+from charmhelpers.fetch import apt_install
2560 from shutil import rmtree
2561 from subprocess import CalledProcessError
2562
2563+from charmhelpers.contrib.charmsupport import nrpe
2564+
2565 from common import (
2566 write_json_file, JujuBroker, LandscapeBroker, chown)
2567 from ceph import (
2568@@ -41,6 +44,7 @@
2569 match the data in the config
2570 """
2571 juju_broker.log("In config-changed for %s" % landscape_broker.local_unit)
2572+ update_nrpe_config()
2573 if relation_data is None:
2574 relation_data = {}
2575 service_config = juju_broker.get_service_config()
2576@@ -60,6 +64,7 @@
2577 def upgrade_charm(landscape_broker=LANDSCAPE_BROKER):
2578 """Idempotently upgrades state from older charms."""
2579 landscape_broker.config.reload()
2580+ update_nrpe_config()
2581 return migrate_old_juju_info(landscape_broker.config)
2582
2583
2584@@ -147,6 +152,24 @@
2585 landscape_broker.clear_registration()
2586
2587
2588+@hooks.hook('nrpe-external-master-relation-joined',
2589+ 'nrpe-external-master-relation-changed')
2590+def update_nrpe_config():
2591+ # python-dbus is used by check_upstart_job
2592+ apt_install('python-dbus')
2593+ hostname = nrpe.get_nagios_hostname()
2594+ current_unit = nrpe.get_nagios_unit_name()
2595+ nrpe_setup = nrpe.NRPE(hostname=hostname)
2596+ nrpe.add_init_service_checks(nrpe_setup, ['landscape-client'], current_unit)
2597+ # process checks
2598+ nrpe_setup.add_check(
2599+ shortname='landscape-client_proc',
2600+ description='Check Landscape Client process {%s}' % current_unit,
2601+ check_cmd='check_procs -c 1: -a /usr/bin/landscape-client'
2602+ )
2603+ nrpe_setup.write()
2604+
2605+
2606 def migrate_old_juju_info(client_config):
2607 """
2608 Migrates data from the old meta-data.d directory into the new
2609
2610=== added symlink 'hooks/nrpe-external-master-relation-changed'
2611=== target is u'hooks.py'
2612=== added symlink 'hooks/nrpe-external-master-relation-joined'
2613=== target is u'hooks.py'
2614=== modified file 'metadata.yaml'
2615--- metadata.yaml 2014-12-05 22:50:09 +0000
2616+++ metadata.yaml 2015-03-31 04:43:29 +0000
2617@@ -8,6 +8,10 @@
2618 Landscape account.
2619 subordinate: true
2620 tags: [ ops, monitoring ]
2621+provides:
2622+ nrpe-external-master:
2623+ interface: nrpe-external-master
2624+ scope: container
2625 requires:
2626 container:
2627 interface: juju-info

Subscribers

People subscribed via source and target branches