Merge lp:~brad-marshall/charms/trusty/percona-cluster/fix-nagios into lp:~openstack-charmers-archive/charms/trusty/percona-cluster/next

Proposed by Brad Marshall
Status: Merged
Merged at revision: 56
Proposed branch: lp:~brad-marshall/charms/trusty/percona-cluster/fix-nagios
Merge into: lp:~openstack-charmers-archive/charms/trusty/percona-cluster/next
Diff against target: 1715 lines (+584/-522)
9 files modified
charm-helpers.yaml (+1/-0)
config.yaml (+16/-1)
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/contrib/database/mysql.py (+0/-2)
hooks/charmhelpers/core/strutils.py (+0/-42)
hooks/charmhelpers/core/unitdata.py (+0/-477)
hooks/percona_hooks.py (+19/-0)
To merge this branch: bzr merge lp:~brad-marshall/charms/trusty/percona-cluster/fix-nagios
Reviewer Review Type Date Requested Status
Liam Young (community) Approve
Brad Marshall (community) Needs Resubmitting
Review via email: mp+250702@code.launchpad.net

Description of the change

Synced charmhelpers, added nagios_servicegroup config option

To post a comment you must log in.
48. By Liam Young

[hopem, r=niedbalski,gnuoy] Synced charm-helpers to get fix for LP #1423153

Revision history for this message
Liam Young (gnuoy) wrote :

update_nrpe_config() never seems to get called. The nrpe-external-master-relation-{joined,changed} symlinks are missing and update_nrpe_config() is not called from anyother hooks

review: Needs Fixing
49. By Billy Olsen

[hopem, r=wolsen] sync charm-helpers fix for LP #1425999

50. By Edward Hope-Morley

Reverted commit r49

51. By Jorge Niedbalski

[hopem, r=billy-olsen, niedbalski] Fixes bug LP: #1425999

52. By Brad Marshall

[bradm] Add nagios checks, sync charmhelpers, merged with upstream

Revision history for this message
Brad Marshall (brad-marshall) wrote :

This should be fixed, and now ready for a re-review

review: Needs Resubmitting
Revision history for this message
Liam Young (gnuoy) wrote :

Approve

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'charm-helpers.yaml'
2--- charm-helpers.yaml 2015-02-10 11:16:27 +0000
3+++ charm-helpers.yaml 2015-03-03 02:26:44 +0000
4@@ -8,3 +8,4 @@
5 - payload.execd
6 - contrib.network.ip
7 - contrib.database
8+ - contrib.charmsupport
9
10=== modified file 'config.yaml'
11--- config.yaml 2015-02-16 14:12:42 +0000
12+++ config.yaml 2015-03-03 02:26:44 +0000
13@@ -77,4 +77,19 @@
14 type: boolean
15 default: False
16 description: Adds two config options (wsrep_drupal_282555_workaround and wsrep_retry_autocommit) as a workaround for Percona Primary Key bug (see lplp1366997).
17-
18+ nagios_context:
19+ default: "juju"
20+ type: string
21+ description: |
22+ Used by the nrpe-external-master subordinate charm.
23+ A string that will be prepended to instance name to set the host name
24+ in nagios. So for instance the hostname would be something like:
25+ juju-myservice-0
26+ If you're running multiple environments with the same services in them
27+ this allows you to differentiate between them.
28+ nagios_servicegroups:
29+ default: ""
30+ type: string
31+ description: |
32+ A comma-separated list of nagios servicegroups.
33+ If left empty, the nagios_context will be used as the servicegroup
34
35=== added directory 'hooks/charmhelpers/contrib/charmsupport'
36=== added file 'hooks/charmhelpers/contrib/charmsupport/__init__.py'
37--- hooks/charmhelpers/contrib/charmsupport/__init__.py 1970-01-01 00:00:00 +0000
38+++ hooks/charmhelpers/contrib/charmsupport/__init__.py 2015-03-03 02:26:44 +0000
39@@ -0,0 +1,15 @@
40+# Copyright 2014-2015 Canonical Limited.
41+#
42+# This file is part of charm-helpers.
43+#
44+# charm-helpers is free software: you can redistribute it and/or modify
45+# it under the terms of the GNU Lesser General Public License version 3 as
46+# published by the Free Software Foundation.
47+#
48+# charm-helpers is distributed in the hope that it will be useful,
49+# but WITHOUT ANY WARRANTY; without even the implied warranty of
50+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
51+# GNU Lesser General Public License for more details.
52+#
53+# You should have received a copy of the GNU Lesser General Public License
54+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
55
56=== added file 'hooks/charmhelpers/contrib/charmsupport/nrpe.py'
57--- hooks/charmhelpers/contrib/charmsupport/nrpe.py 1970-01-01 00:00:00 +0000
58+++ hooks/charmhelpers/contrib/charmsupport/nrpe.py 2015-03-03 02:26:44 +0000
59@@ -0,0 +1,358 @@
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+"""Compatibility with the nrpe-external-master charm"""
77+# Copyright 2012 Canonical Ltd.
78+#
79+# Authors:
80+# Matthew Wedgwood <matthew.wedgwood@canonical.com>
81+
82+import subprocess
83+import pwd
84+import grp
85+import os
86+import glob
87+import shutil
88+import re
89+import shlex
90+import yaml
91+
92+from charmhelpers.core.hookenv import (
93+ config,
94+ local_unit,
95+ log,
96+ relation_ids,
97+ relation_set,
98+ relations_of_type,
99+)
100+
101+from charmhelpers.core.host import service
102+
103+# This module adds compatibility with the nrpe-external-master and plain nrpe
104+# subordinate charms. To use it in your charm:
105+#
106+# 1. Update metadata.yaml
107+#
108+# provides:
109+# (...)
110+# nrpe-external-master:
111+# interface: nrpe-external-master
112+# scope: container
113+#
114+# and/or
115+#
116+# provides:
117+# (...)
118+# local-monitors:
119+# interface: local-monitors
120+# scope: container
121+
122+#
123+# 2. Add the following to config.yaml
124+#
125+# nagios_context:
126+# default: "juju"
127+# type: string
128+# description: |
129+# Used by the nrpe subordinate charms.
130+# A string that will be prepended to instance name to set the host name
131+# in nagios. So for instance the hostname would be something like:
132+# juju-myservice-0
133+# If you're running multiple environments with the same services in them
134+# this allows you to differentiate between them.
135+# nagios_servicegroups:
136+# default: ""
137+# type: string
138+# description: |
139+# A comma-separated list of nagios servicegroups.
140+# If left empty, the nagios_context will be used as the servicegroup
141+#
142+# 3. Add custom checks (Nagios plugins) to files/nrpe-external-master
143+#
144+# 4. Update your hooks.py with something like this:
145+#
146+# from charmsupport.nrpe import NRPE
147+# (...)
148+# def update_nrpe_config():
149+# nrpe_compat = NRPE()
150+# nrpe_compat.add_check(
151+# shortname = "myservice",
152+# description = "Check MyService",
153+# check_cmd = "check_http -w 2 -c 10 http://localhost"
154+# )
155+# nrpe_compat.add_check(
156+# "myservice_other",
157+# "Check for widget failures",
158+# check_cmd = "/srv/myapp/scripts/widget_check"
159+# )
160+# nrpe_compat.write()
161+#
162+# def config_changed():
163+# (...)
164+# update_nrpe_config()
165+#
166+# def nrpe_external_master_relation_changed():
167+# update_nrpe_config()
168+#
169+# def local_monitors_relation_changed():
170+# update_nrpe_config()
171+#
172+# 5. ln -s hooks.py nrpe-external-master-relation-changed
173+# ln -s hooks.py local-monitors-relation-changed
174+
175+
176+class CheckException(Exception):
177+ pass
178+
179+
180+class Check(object):
181+ shortname_re = '[A-Za-z0-9-_]+$'
182+ service_template = ("""
183+#---------------------------------------------------
184+# This file is Juju managed
185+#---------------------------------------------------
186+define service {{
187+ use active-service
188+ host_name {nagios_hostname}
189+ service_description {nagios_hostname}[{shortname}] """
190+ """{description}
191+ check_command check_nrpe!{command}
192+ servicegroups {nagios_servicegroup}
193+}}
194+""")
195+
196+ def __init__(self, shortname, description, check_cmd):
197+ super(Check, self).__init__()
198+ # XXX: could be better to calculate this from the service name
199+ if not re.match(self.shortname_re, shortname):
200+ raise CheckException("shortname must match {}".format(
201+ Check.shortname_re))
202+ self.shortname = shortname
203+ self.command = "check_{}".format(shortname)
204+ # Note: a set of invalid characters is defined by the
205+ # Nagios server config
206+ # The default is: illegal_object_name_chars=`~!$%^&*"|'<>?,()=
207+ self.description = description
208+ self.check_cmd = self._locate_cmd(check_cmd)
209+
210+ def _locate_cmd(self, check_cmd):
211+ search_path = (
212+ '/usr/lib/nagios/plugins',
213+ '/usr/local/lib/nagios/plugins',
214+ )
215+ parts = shlex.split(check_cmd)
216+ for path in search_path:
217+ if os.path.exists(os.path.join(path, parts[0])):
218+ command = os.path.join(path, parts[0])
219+ if len(parts) > 1:
220+ command += " " + " ".join(parts[1:])
221+ return command
222+ log('Check command not found: {}'.format(parts[0]))
223+ return ''
224+
225+ def write(self, nagios_context, hostname, nagios_servicegroups):
226+ nrpe_check_file = '/etc/nagios/nrpe.d/{}.cfg'.format(
227+ self.command)
228+ with open(nrpe_check_file, 'w') as nrpe_check_config:
229+ nrpe_check_config.write("# check {}\n".format(self.shortname))
230+ nrpe_check_config.write("command[{}]={}\n".format(
231+ self.command, self.check_cmd))
232+
233+ if not os.path.exists(NRPE.nagios_exportdir):
234+ log('Not writing service config as {} is not accessible'.format(
235+ NRPE.nagios_exportdir))
236+ else:
237+ self.write_service_config(nagios_context, hostname,
238+ nagios_servicegroups)
239+
240+ def write_service_config(self, nagios_context, hostname,
241+ nagios_servicegroups):
242+ for f in os.listdir(NRPE.nagios_exportdir):
243+ if re.search('.*{}.cfg'.format(self.command), f):
244+ os.remove(os.path.join(NRPE.nagios_exportdir, f))
245+
246+ templ_vars = {
247+ 'nagios_hostname': hostname,
248+ 'nagios_servicegroup': nagios_servicegroups,
249+ 'description': self.description,
250+ 'shortname': self.shortname,
251+ 'command': self.command,
252+ }
253+ nrpe_service_text = Check.service_template.format(**templ_vars)
254+ nrpe_service_file = '{}/service__{}_{}.cfg'.format(
255+ NRPE.nagios_exportdir, hostname, self.command)
256+ with open(nrpe_service_file, 'w') as nrpe_service_config:
257+ nrpe_service_config.write(str(nrpe_service_text))
258+
259+ def run(self):
260+ subprocess.call(self.check_cmd)
261+
262+
263+class NRPE(object):
264+ nagios_logdir = '/var/log/nagios'
265+ nagios_exportdir = '/var/lib/nagios/export'
266+ nrpe_confdir = '/etc/nagios/nrpe.d'
267+
268+ def __init__(self, hostname=None):
269+ super(NRPE, self).__init__()
270+ self.config = config()
271+ self.nagios_context = self.config['nagios_context']
272+ if 'nagios_servicegroups' in self.config and self.config['nagios_servicegroups']:
273+ self.nagios_servicegroups = self.config['nagios_servicegroups']
274+ else:
275+ self.nagios_servicegroups = self.nagios_context
276+ self.unit_name = local_unit().replace('/', '-')
277+ if hostname:
278+ self.hostname = hostname
279+ else:
280+ self.hostname = "{}-{}".format(self.nagios_context, self.unit_name)
281+ self.checks = []
282+
283+ def add_check(self, *args, **kwargs):
284+ self.checks.append(Check(*args, **kwargs))
285+
286+ def write(self):
287+ try:
288+ nagios_uid = pwd.getpwnam('nagios').pw_uid
289+ nagios_gid = grp.getgrnam('nagios').gr_gid
290+ except:
291+ log("Nagios user not set up, nrpe checks not updated")
292+ return
293+
294+ if not os.path.exists(NRPE.nagios_logdir):
295+ os.mkdir(NRPE.nagios_logdir)
296+ os.chown(NRPE.nagios_logdir, nagios_uid, nagios_gid)
297+
298+ nrpe_monitors = {}
299+ monitors = {"monitors": {"remote": {"nrpe": nrpe_monitors}}}
300+ for nrpecheck in self.checks:
301+ nrpecheck.write(self.nagios_context, self.hostname,
302+ self.nagios_servicegroups)
303+ nrpe_monitors[nrpecheck.shortname] = {
304+ "command": nrpecheck.command,
305+ }
306+
307+ service('restart', 'nagios-nrpe-server')
308+
309+ for rid in relation_ids("local-monitors"):
310+ relation_set(relation_id=rid, monitors=yaml.dump(monitors))
311+
312+
313+def get_nagios_hostcontext(relation_name='nrpe-external-master'):
314+ """
315+ Query relation with nrpe subordinate, return the nagios_host_context
316+
317+ :param str relation_name: Name of relation nrpe sub joined to
318+ """
319+ for rel in relations_of_type(relation_name):
320+ if 'nagios_hostname' in rel:
321+ return rel['nagios_host_context']
322+
323+
324+def get_nagios_hostname(relation_name='nrpe-external-master'):
325+ """
326+ Query relation with nrpe subordinate, return the nagios_hostname
327+
328+ :param str relation_name: Name of relation nrpe sub joined to
329+ """
330+ for rel in relations_of_type(relation_name):
331+ if 'nagios_hostname' in rel:
332+ return rel['nagios_hostname']
333+
334+
335+def get_nagios_unit_name(relation_name='nrpe-external-master'):
336+ """
337+ Return the nagios unit name prepended with host_context if needed
338+
339+ :param str relation_name: Name of relation nrpe sub joined to
340+ """
341+ host_context = get_nagios_hostcontext(relation_name)
342+ if host_context:
343+ unit = "%s:%s" % (host_context, local_unit())
344+ else:
345+ unit = local_unit()
346+ return unit
347+
348+
349+def add_init_service_checks(nrpe, services, unit_name):
350+ """
351+ Add checks for each service in list
352+
353+ :param NRPE nrpe: NRPE object to add check to
354+ :param list services: List of services to check
355+ :param str unit_name: Unit name to use in check description
356+ """
357+ for svc in services:
358+ upstart_init = '/etc/init/%s.conf' % svc
359+ sysv_init = '/etc/init.d/%s' % svc
360+ if os.path.exists(upstart_init):
361+ nrpe.add_check(
362+ shortname=svc,
363+ description='process check {%s}' % unit_name,
364+ check_cmd='check_upstart_job %s' % svc
365+ )
366+ elif os.path.exists(sysv_init):
367+ cronpath = '/etc/cron.d/nagios-service-check-%s' % svc
368+ cron_file = ('*/5 * * * * root '
369+ '/usr/local/lib/nagios/plugins/check_exit_status.pl '
370+ '-s /etc/init.d/%s status > '
371+ '/var/lib/nagios/service-check-%s.txt\n' % (svc,
372+ svc)
373+ )
374+ f = open(cronpath, 'w')
375+ f.write(cron_file)
376+ f.close()
377+ nrpe.add_check(
378+ shortname=svc,
379+ description='process check {%s}' % unit_name,
380+ check_cmd='check_status_file.py -f '
381+ '/var/lib/nagios/service-check-%s.txt' % svc,
382+ )
383+
384+
385+def copy_nrpe_checks():
386+ """
387+ Copy the nrpe checks into place
388+
389+ """
390+ NAGIOS_PLUGINS = '/usr/local/lib/nagios/plugins'
391+ nrpe_files_dir = os.path.join(os.getenv('CHARM_DIR'), 'hooks',
392+ 'charmhelpers', 'contrib', 'openstack',
393+ 'files')
394+
395+ if not os.path.exists(NAGIOS_PLUGINS):
396+ os.makedirs(NAGIOS_PLUGINS)
397+ for fname in glob.glob(os.path.join(nrpe_files_dir, "check_*")):
398+ if os.path.isfile(fname):
399+ shutil.copy2(fname,
400+ os.path.join(NAGIOS_PLUGINS, os.path.basename(fname)))
401+
402+
403+def add_haproxy_checks(nrpe, unit_name):
404+ """
405+ Add checks for each service in list
406+
407+ :param NRPE nrpe: NRPE object to add check to
408+ :param str unit_name: Unit name to use in check description
409+ """
410+ nrpe.add_check(
411+ shortname='haproxy_servers',
412+ description='Check HAProxy {%s}' % unit_name,
413+ check_cmd='check_haproxy.sh')
414+ nrpe.add_check(
415+ shortname='haproxy_queue',
416+ description='Check HAProxy queue depth {%s}' % unit_name,
417+ check_cmd='check_haproxy_queue_depth.sh')
418
419=== added file 'hooks/charmhelpers/contrib/charmsupport/volumes.py'
420--- hooks/charmhelpers/contrib/charmsupport/volumes.py 1970-01-01 00:00:00 +0000
421+++ hooks/charmhelpers/contrib/charmsupport/volumes.py 2015-03-03 02:26:44 +0000
422@@ -0,0 +1,175 @@
423+# Copyright 2014-2015 Canonical Limited.
424+#
425+# This file is part of charm-helpers.
426+#
427+# charm-helpers is free software: you can redistribute it and/or modify
428+# it under the terms of the GNU Lesser General Public License version 3 as
429+# published by the Free Software Foundation.
430+#
431+# charm-helpers is distributed in the hope that it will be useful,
432+# but WITHOUT ANY WARRANTY; without even the implied warranty of
433+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
434+# GNU Lesser General Public License for more details.
435+#
436+# You should have received a copy of the GNU Lesser General Public License
437+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
438+
439+'''
440+Functions for managing volumes in juju units. One volume is supported per unit.
441+Subordinates may have their own storage, provided it is on its own partition.
442+
443+Configuration stanzas::
444+
445+ volume-ephemeral:
446+ type: boolean
447+ default: true
448+ description: >
449+ If false, a volume is mounted as sepecified in "volume-map"
450+ If true, ephemeral storage will be used, meaning that log data
451+ will only exist as long as the machine. YOU HAVE BEEN WARNED.
452+ volume-map:
453+ type: string
454+ default: {}
455+ description: >
456+ YAML map of units to device names, e.g:
457+ "{ rsyslog/0: /dev/vdb, rsyslog/1: /dev/vdb }"
458+ Service units will raise a configure-error if volume-ephemeral
459+ is 'true' and no volume-map value is set. Use 'juju set' to set a
460+ value and 'juju resolved' to complete configuration.
461+
462+Usage::
463+
464+ from charmsupport.volumes import configure_volume, VolumeConfigurationError
465+ from charmsupport.hookenv import log, ERROR
466+ def post_mount_hook():
467+ stop_service('myservice')
468+ def post_mount_hook():
469+ start_service('myservice')
470+
471+ if __name__ == '__main__':
472+ try:
473+ configure_volume(before_change=pre_mount_hook,
474+ after_change=post_mount_hook)
475+ except VolumeConfigurationError:
476+ log('Storage could not be configured', ERROR)
477+
478+'''
479+
480+# XXX: Known limitations
481+# - fstab is neither consulted nor updated
482+
483+import os
484+from charmhelpers.core import hookenv
485+from charmhelpers.core import host
486+import yaml
487+
488+
489+MOUNT_BASE = '/srv/juju/volumes'
490+
491+
492+class VolumeConfigurationError(Exception):
493+ '''Volume configuration data is missing or invalid'''
494+ pass
495+
496+
497+def get_config():
498+ '''Gather and sanity-check volume configuration data'''
499+ volume_config = {}
500+ config = hookenv.config()
501+
502+ errors = False
503+
504+ if config.get('volume-ephemeral') in (True, 'True', 'true', 'Yes', 'yes'):
505+ volume_config['ephemeral'] = True
506+ else:
507+ volume_config['ephemeral'] = False
508+
509+ try:
510+ volume_map = yaml.safe_load(config.get('volume-map', '{}'))
511+ except yaml.YAMLError as e:
512+ hookenv.log("Error parsing YAML volume-map: {}".format(e),
513+ hookenv.ERROR)
514+ errors = True
515+ if volume_map is None:
516+ # probably an empty string
517+ volume_map = {}
518+ elif not isinstance(volume_map, dict):
519+ hookenv.log("Volume-map should be a dictionary, not {}".format(
520+ type(volume_map)))
521+ errors = True
522+
523+ volume_config['device'] = volume_map.get(os.environ['JUJU_UNIT_NAME'])
524+ if volume_config['device'] and volume_config['ephemeral']:
525+ # asked for ephemeral storage but also defined a volume ID
526+ hookenv.log('A volume is defined for this unit, but ephemeral '
527+ 'storage was requested', hookenv.ERROR)
528+ errors = True
529+ elif not volume_config['device'] and not volume_config['ephemeral']:
530+ # asked for permanent storage but did not define volume ID
531+ hookenv.log('Ephemeral storage was requested, but there is no volume '
532+ 'defined for this unit.', hookenv.ERROR)
533+ errors = True
534+
535+ unit_mount_name = hookenv.local_unit().replace('/', '-')
536+ volume_config['mountpoint'] = os.path.join(MOUNT_BASE, unit_mount_name)
537+
538+ if errors:
539+ return None
540+ return volume_config
541+
542+
543+def mount_volume(config):
544+ if os.path.exists(config['mountpoint']):
545+ if not os.path.isdir(config['mountpoint']):
546+ hookenv.log('Not a directory: {}'.format(config['mountpoint']))
547+ raise VolumeConfigurationError()
548+ else:
549+ host.mkdir(config['mountpoint'])
550+ if os.path.ismount(config['mountpoint']):
551+ unmount_volume(config)
552+ if not host.mount(config['device'], config['mountpoint'], persist=True):
553+ raise VolumeConfigurationError()
554+
555+
556+def unmount_volume(config):
557+ if os.path.ismount(config['mountpoint']):
558+ if not host.umount(config['mountpoint'], persist=True):
559+ raise VolumeConfigurationError()
560+
561+
562+def managed_mounts():
563+ '''List of all mounted managed volumes'''
564+ return filter(lambda mount: mount[0].startswith(MOUNT_BASE), host.mounts())
565+
566+
567+def configure_volume(before_change=lambda: None, after_change=lambda: None):
568+ '''Set up storage (or don't) according to the charm's volume configuration.
569+ Returns the mount point or "ephemeral". before_change and after_change
570+ are optional functions to be called if the volume configuration changes.
571+ '''
572+
573+ config = get_config()
574+ if not config:
575+ hookenv.log('Failed to read volume configuration', hookenv.CRITICAL)
576+ raise VolumeConfigurationError()
577+
578+ if config['ephemeral']:
579+ if os.path.ismount(config['mountpoint']):
580+ before_change()
581+ unmount_volume(config)
582+ after_change()
583+ return 'ephemeral'
584+ else:
585+ # persistent storage
586+ if os.path.ismount(config['mountpoint']):
587+ mounts = dict(managed_mounts())
588+ if mounts.get(config['mountpoint']) != config['device']:
589+ before_change()
590+ unmount_volume(config)
591+ mount_volume(config)
592+ after_change()
593+ else:
594+ before_change()
595+ mount_volume(config)
596+ after_change()
597+ return config['mountpoint']
598
599=== modified file 'hooks/charmhelpers/contrib/database/mysql.py'
600--- hooks/charmhelpers/contrib/database/mysql.py 2015-02-27 10:17:54 +0000
601+++ hooks/charmhelpers/contrib/database/mysql.py 2015-03-03 02:26:44 +0000
602@@ -1,6 +1,5 @@
603 """Helper for working with a MySQL database"""
604 import json
605-import socket
606 import re
607 import sys
608 import platform
609@@ -22,7 +21,6 @@
610 log,
611 DEBUG,
612 INFO,
613- WARNING,
614 )
615 from charmhelpers.fetch import (
616 apt_install,
617
618=== added file 'hooks/charmhelpers/core/strutils.py'
619--- hooks/charmhelpers/core/strutils.py 1970-01-01 00:00:00 +0000
620+++ hooks/charmhelpers/core/strutils.py 2015-03-03 02:26:44 +0000
621@@ -0,0 +1,42 @@
622+#!/usr/bin/env python
623+# -*- coding: utf-8 -*-
624+
625+# Copyright 2014-2015 Canonical Limited.
626+#
627+# This file is part of charm-helpers.
628+#
629+# charm-helpers is free software: you can redistribute it and/or modify
630+# it under the terms of the GNU Lesser General Public License version 3 as
631+# published by the Free Software Foundation.
632+#
633+# charm-helpers is distributed in the hope that it will be useful,
634+# but WITHOUT ANY WARRANTY; without even the implied warranty of
635+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
636+# GNU Lesser General Public License for more details.
637+#
638+# You should have received a copy of the GNU Lesser General Public License
639+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
640+
641+import six
642+
643+
644+def bool_from_string(value):
645+ """Interpret string value as boolean.
646+
647+ Returns True if value translates to True otherwise False.
648+ """
649+ if isinstance(value, six.string_types):
650+ value = six.text_type(value)
651+ else:
652+ msg = "Unable to interpret non-string value '%s' as boolean" % (value)
653+ raise ValueError(msg)
654+
655+ value = value.strip().lower()
656+
657+ if value in ['y', 'yes', 'true', 't']:
658+ return True
659+ elif value in ['n', 'no', 'false', 'f']:
660+ return False
661+
662+ msg = "Unable to interpret string value '%s' as boolean" % (value)
663+ raise ValueError(msg)
664
665=== removed file 'hooks/charmhelpers/core/strutils.py'
666--- hooks/charmhelpers/core/strutils.py 2015-02-25 20:48:34 +0000
667+++ hooks/charmhelpers/core/strutils.py 1970-01-01 00:00:00 +0000
668@@ -1,42 +0,0 @@
669-#!/usr/bin/env python
670-# -*- coding: utf-8 -*-
671-
672-# Copyright 2014-2015 Canonical Limited.
673-#
674-# This file is part of charm-helpers.
675-#
676-# charm-helpers is free software: you can redistribute it and/or modify
677-# it under the terms of the GNU Lesser General Public License version 3 as
678-# published by the Free Software Foundation.
679-#
680-# charm-helpers is distributed in the hope that it will be useful,
681-# but WITHOUT ANY WARRANTY; without even the implied warranty of
682-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
683-# GNU Lesser General Public License for more details.
684-#
685-# You should have received a copy of the GNU Lesser General Public License
686-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
687-
688-import six
689-
690-
691-def bool_from_string(value):
692- """Interpret string value as boolean.
693-
694- Returns True if value translates to True otherwise False.
695- """
696- if isinstance(value, six.string_types):
697- value = six.text_type(value)
698- else:
699- msg = "Unable to interpret non-string value '%s' as boolean" % (value)
700- raise ValueError(msg)
701-
702- value = value.strip().lower()
703-
704- if value in ['y', 'yes', 'true', 't']:
705- return True
706- elif value in ['n', 'no', 'false', 'f']:
707- return False
708-
709- msg = "Unable to interpret string value '%s' as boolean" % (value)
710- raise ValueError(msg)
711
712=== added file 'hooks/charmhelpers/core/unitdata.py'
713--- hooks/charmhelpers/core/unitdata.py 1970-01-01 00:00:00 +0000
714+++ hooks/charmhelpers/core/unitdata.py 2015-03-03 02:26:44 +0000
715@@ -0,0 +1,477 @@
716+#!/usr/bin/env python
717+# -*- coding: utf-8 -*-
718+#
719+# Copyright 2014-2015 Canonical Limited.
720+#
721+# This file is part of charm-helpers.
722+#
723+# charm-helpers is free software: you can redistribute it and/or modify
724+# it under the terms of the GNU Lesser General Public License version 3 as
725+# published by the Free Software Foundation.
726+#
727+# charm-helpers is distributed in the hope that it will be useful,
728+# but WITHOUT ANY WARRANTY; without even the implied warranty of
729+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
730+# GNU Lesser General Public License for more details.
731+#
732+# You should have received a copy of the GNU Lesser General Public License
733+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
734+#
735+#
736+# Authors:
737+# Kapil Thangavelu <kapil.foss@gmail.com>
738+#
739+"""
740+Intro
741+-----
742+
743+A simple way to store state in units. This provides a key value
744+storage with support for versioned, transactional operation,
745+and can calculate deltas from previous values to simplify unit logic
746+when processing changes.
747+
748+
749+Hook Integration
750+----------------
751+
752+There are several extant frameworks for hook execution, including
753+
754+ - charmhelpers.core.hookenv.Hooks
755+ - charmhelpers.core.services.ServiceManager
756+
757+The storage classes are framework agnostic, one simple integration is
758+via the HookData contextmanager. It will record the current hook
759+execution environment (including relation data, config data, etc.),
760+setup a transaction and allow easy access to the changes from
761+previously seen values. One consequence of the integration is the
762+reservation of particular keys ('rels', 'unit', 'env', 'config',
763+'charm_revisions') for their respective values.
764+
765+Here's a fully worked integration example using hookenv.Hooks::
766+
767+ from charmhelper.core import hookenv, unitdata
768+
769+ hook_data = unitdata.HookData()
770+ db = unitdata.kv()
771+ hooks = hookenv.Hooks()
772+
773+ @hooks.hook
774+ def config_changed():
775+ # Print all changes to configuration from previously seen
776+ # values.
777+ for changed, (prev, cur) in hook_data.conf.items():
778+ print('config changed', changed,
779+ 'previous value', prev,
780+ 'current value', cur)
781+
782+ # Get some unit specific bookeeping
783+ if not db.get('pkg_key'):
784+ key = urllib.urlopen('https://example.com/pkg_key').read()
785+ db.set('pkg_key', key)
786+
787+ # Directly access all charm config as a mapping.
788+ conf = db.getrange('config', True)
789+
790+ # Directly access all relation data as a mapping
791+ rels = db.getrange('rels', True)
792+
793+ if __name__ == '__main__':
794+ with hook_data():
795+ hook.execute()
796+
797+
798+A more basic integration is via the hook_scope context manager which simply
799+manages transaction scope (and records hook name, and timestamp)::
800+
801+ >>> from unitdata import kv
802+ >>> db = kv()
803+ >>> with db.hook_scope('install'):
804+ ... # do work, in transactional scope.
805+ ... db.set('x', 1)
806+ >>> db.get('x')
807+ 1
808+
809+
810+Usage
811+-----
812+
813+Values are automatically json de/serialized to preserve basic typing
814+and complex data struct capabilities (dicts, lists, ints, booleans, etc).
815+
816+Individual values can be manipulated via get/set::
817+
818+ >>> kv.set('y', True)
819+ >>> kv.get('y')
820+ True
821+
822+ # We can set complex values (dicts, lists) as a single key.
823+ >>> kv.set('config', {'a': 1, 'b': True'})
824+
825+ # Also supports returning dictionaries as a record which
826+ # provides attribute access.
827+ >>> config = kv.get('config', record=True)
828+ >>> config.b
829+ True
830+
831+
832+Groups of keys can be manipulated with update/getrange::
833+
834+ >>> kv.update({'z': 1, 'y': 2}, prefix="gui.")
835+ >>> kv.getrange('gui.', strip=True)
836+ {'z': 1, 'y': 2}
837+
838+When updating values, its very helpful to understand which values
839+have actually changed and how have they changed. The storage
840+provides a delta method to provide for this::
841+
842+ >>> data = {'debug': True, 'option': 2}
843+ >>> delta = kv.delta(data, 'config.')
844+ >>> delta.debug.previous
845+ None
846+ >>> delta.debug.current
847+ True
848+ >>> delta
849+ {'debug': (None, True), 'option': (None, 2)}
850+
851+Note the delta method does not persist the actual change, it needs to
852+be explicitly saved via 'update' method::
853+
854+ >>> kv.update(data, 'config.')
855+
856+Values modified in the context of a hook scope retain historical values
857+associated to the hookname.
858+
859+ >>> with db.hook_scope('config-changed'):
860+ ... db.set('x', 42)
861+ >>> db.gethistory('x')
862+ [(1, u'x', 1, u'install', u'2015-01-21T16:49:30.038372'),
863+ (2, u'x', 42, u'config-changed', u'2015-01-21T16:49:30.038786')]
864+
865+"""
866+
867+import collections
868+import contextlib
869+import datetime
870+import json
871+import os
872+import pprint
873+import sqlite3
874+import sys
875+
876+__author__ = 'Kapil Thangavelu <kapil.foss@gmail.com>'
877+
878+
879+class Storage(object):
880+ """Simple key value database for local unit state within charms.
881+
882+ Modifications are automatically committed at hook exit. That's
883+ currently regardless of exit code.
884+
885+ To support dicts, lists, integer, floats, and booleans values
886+ are automatically json encoded/decoded.
887+ """
888+ def __init__(self, path=None):
889+ self.db_path = path
890+ if path is None:
891+ self.db_path = os.path.join(
892+ os.environ.get('CHARM_DIR', ''), '.unit-state.db')
893+ self.conn = sqlite3.connect('%s' % self.db_path)
894+ self.cursor = self.conn.cursor()
895+ self.revision = None
896+ self._closed = False
897+ self._init()
898+
899+ def close(self):
900+ if self._closed:
901+ return
902+ self.flush(False)
903+ self.cursor.close()
904+ self.conn.close()
905+ self._closed = True
906+
907+ def _scoped_query(self, stmt, params=None):
908+ if params is None:
909+ params = []
910+ return stmt, params
911+
912+ def get(self, key, default=None, record=False):
913+ self.cursor.execute(
914+ *self._scoped_query(
915+ 'select data from kv where key=?', [key]))
916+ result = self.cursor.fetchone()
917+ if not result:
918+ return default
919+ if record:
920+ return Record(json.loads(result[0]))
921+ return json.loads(result[0])
922+
923+ def getrange(self, key_prefix, strip=False):
924+ stmt = "select key, data from kv where key like '%s%%'" % key_prefix
925+ self.cursor.execute(*self._scoped_query(stmt))
926+ result = self.cursor.fetchall()
927+
928+ if not result:
929+ return None
930+ if not strip:
931+ key_prefix = ''
932+ return dict([
933+ (k[len(key_prefix):], json.loads(v)) for k, v in result])
934+
935+ def update(self, mapping, prefix=""):
936+ for k, v in mapping.items():
937+ self.set("%s%s" % (prefix, k), v)
938+
939+ def unset(self, key):
940+ self.cursor.execute('delete from kv where key=?', [key])
941+ if self.revision and self.cursor.rowcount:
942+ self.cursor.execute(
943+ 'insert into kv_revisions values (?, ?, ?)',
944+ [key, self.revision, json.dumps('DELETED')])
945+
946+ def set(self, key, value):
947+ serialized = json.dumps(value)
948+
949+ self.cursor.execute(
950+ 'select data from kv where key=?', [key])
951+ exists = self.cursor.fetchone()
952+
953+ # Skip mutations to the same value
954+ if exists:
955+ if exists[0] == serialized:
956+ return value
957+
958+ if not exists:
959+ self.cursor.execute(
960+ 'insert into kv (key, data) values (?, ?)',
961+ (key, serialized))
962+ else:
963+ self.cursor.execute('''
964+ update kv
965+ set data = ?
966+ where key = ?''', [serialized, key])
967+
968+ # Save
969+ if not self.revision:
970+ return value
971+
972+ self.cursor.execute(
973+ 'select 1 from kv_revisions where key=? and revision=?',
974+ [key, self.revision])
975+ exists = self.cursor.fetchone()
976+
977+ if not exists:
978+ self.cursor.execute(
979+ '''insert into kv_revisions (
980+ revision, key, data) values (?, ?, ?)''',
981+ (self.revision, key, serialized))
982+ else:
983+ self.cursor.execute(
984+ '''
985+ update kv_revisions
986+ set data = ?
987+ where key = ?
988+ and revision = ?''',
989+ [serialized, key, self.revision])
990+
991+ return value
992+
993+ def delta(self, mapping, prefix):
994+ """
995+ return a delta containing values that have changed.
996+ """
997+ previous = self.getrange(prefix, strip=True)
998+ if not previous:
999+ pk = set()
1000+ else:
1001+ pk = set(previous.keys())
1002+ ck = set(mapping.keys())
1003+ delta = DeltaSet()
1004+
1005+ # added
1006+ for k in ck.difference(pk):
1007+ delta[k] = Delta(None, mapping[k])
1008+
1009+ # removed
1010+ for k in pk.difference(ck):
1011+ delta[k] = Delta(previous[k], None)
1012+
1013+ # changed
1014+ for k in pk.intersection(ck):
1015+ c = mapping[k]
1016+ p = previous[k]
1017+ if c != p:
1018+ delta[k] = Delta(p, c)
1019+
1020+ return delta
1021+
1022+ @contextlib.contextmanager
1023+ def hook_scope(self, name=""):
1024+ """Scope all future interactions to the current hook execution
1025+ revision."""
1026+ assert not self.revision
1027+ self.cursor.execute(
1028+ 'insert into hooks (hook, date) values (?, ?)',
1029+ (name or sys.argv[0],
1030+ datetime.datetime.utcnow().isoformat()))
1031+ self.revision = self.cursor.lastrowid
1032+ try:
1033+ yield self.revision
1034+ self.revision = None
1035+ except:
1036+ self.flush(False)
1037+ self.revision = None
1038+ raise
1039+ else:
1040+ self.flush()
1041+
1042+ def flush(self, save=True):
1043+ if save:
1044+ self.conn.commit()
1045+ elif self._closed:
1046+ return
1047+ else:
1048+ self.conn.rollback()
1049+
1050+ def _init(self):
1051+ self.cursor.execute('''
1052+ create table if not exists kv (
1053+ key text,
1054+ data text,
1055+ primary key (key)
1056+ )''')
1057+ self.cursor.execute('''
1058+ create table if not exists kv_revisions (
1059+ key text,
1060+ revision integer,
1061+ data text,
1062+ primary key (key, revision)
1063+ )''')
1064+ self.cursor.execute('''
1065+ create table if not exists hooks (
1066+ version integer primary key autoincrement,
1067+ hook text,
1068+ date text
1069+ )''')
1070+ self.conn.commit()
1071+
1072+ def gethistory(self, key, deserialize=False):
1073+ self.cursor.execute(
1074+ '''
1075+ select kv.revision, kv.key, kv.data, h.hook, h.date
1076+ from kv_revisions kv,
1077+ hooks h
1078+ where kv.key=?
1079+ and kv.revision = h.version
1080+ ''', [key])
1081+ if deserialize is False:
1082+ return self.cursor.fetchall()
1083+ return map(_parse_history, self.cursor.fetchall())
1084+
1085+ def debug(self, fh=sys.stderr):
1086+ self.cursor.execute('select * from kv')
1087+ pprint.pprint(self.cursor.fetchall(), stream=fh)
1088+ self.cursor.execute('select * from kv_revisions')
1089+ pprint.pprint(self.cursor.fetchall(), stream=fh)
1090+
1091+
1092+def _parse_history(d):
1093+ return (d[0], d[1], json.loads(d[2]), d[3],
1094+ datetime.datetime.strptime(d[-1], "%Y-%m-%dT%H:%M:%S.%f"))
1095+
1096+
1097+class HookData(object):
1098+ """Simple integration for existing hook exec frameworks.
1099+
1100+ Records all unit information, and stores deltas for processing
1101+ by the hook.
1102+
1103+ Sample::
1104+
1105+ from charmhelper.core import hookenv, unitdata
1106+
1107+ changes = unitdata.HookData()
1108+ db = unitdata.kv()
1109+ hooks = hookenv.Hooks()
1110+
1111+ @hooks.hook
1112+ def config_changed():
1113+ # View all changes to configuration
1114+ for changed, (prev, cur) in changes.conf.items():
1115+ print('config changed', changed,
1116+ 'previous value', prev,
1117+ 'current value', cur)
1118+
1119+ # Get some unit specific bookeeping
1120+ if not db.get('pkg_key'):
1121+ key = urllib.urlopen('https://example.com/pkg_key').read()
1122+ db.set('pkg_key', key)
1123+
1124+ if __name__ == '__main__':
1125+ with changes():
1126+ hook.execute()
1127+
1128+ """
1129+ def __init__(self):
1130+ self.kv = kv()
1131+ self.conf = None
1132+ self.rels = None
1133+
1134+ @contextlib.contextmanager
1135+ def __call__(self):
1136+ from charmhelpers.core import hookenv
1137+ hook_name = hookenv.hook_name()
1138+
1139+ with self.kv.hook_scope(hook_name):
1140+ self._record_charm_version(hookenv.charm_dir())
1141+ delta_config, delta_relation = self._record_hook(hookenv)
1142+ yield self.kv, delta_config, delta_relation
1143+
1144+ def _record_charm_version(self, charm_dir):
1145+ # Record revisions.. charm revisions are meaningless
1146+ # to charm authors as they don't control the revision.
1147+ # so logic dependnent on revision is not particularly
1148+ # useful, however it is useful for debugging analysis.
1149+ charm_rev = open(
1150+ os.path.join(charm_dir, 'revision')).read().strip()
1151+ charm_rev = charm_rev or '0'
1152+ revs = self.kv.get('charm_revisions', [])
1153+ if charm_rev not in revs:
1154+ revs.append(charm_rev.strip() or '0')
1155+ self.kv.set('charm_revisions', revs)
1156+
1157+ def _record_hook(self, hookenv):
1158+ data = hookenv.execution_environment()
1159+ self.conf = conf_delta = self.kv.delta(data['conf'], 'config')
1160+ self.rels = rels_delta = self.kv.delta(data['rels'], 'rels')
1161+ self.kv.set('env', data['env'])
1162+ self.kv.set('unit', data['unit'])
1163+ self.kv.set('relid', data.get('relid'))
1164+ return conf_delta, rels_delta
1165+
1166+
1167+class Record(dict):
1168+
1169+ __slots__ = ()
1170+
1171+ def __getattr__(self, k):
1172+ if k in self:
1173+ return self[k]
1174+ raise AttributeError(k)
1175+
1176+
1177+class DeltaSet(Record):
1178+
1179+ __slots__ = ()
1180+
1181+
1182+Delta = collections.namedtuple('Delta', ['previous', 'current'])
1183+
1184+
1185+_KV = None
1186+
1187+
1188+def kv():
1189+ global _KV
1190+ if _KV is None:
1191+ _KV = Storage()
1192+ return _KV
1193
1194=== removed file 'hooks/charmhelpers/core/unitdata.py'
1195--- hooks/charmhelpers/core/unitdata.py 2015-02-25 20:48:34 +0000
1196+++ hooks/charmhelpers/core/unitdata.py 1970-01-01 00:00:00 +0000
1197@@ -1,477 +0,0 @@
1198-#!/usr/bin/env python
1199-# -*- coding: utf-8 -*-
1200-#
1201-# Copyright 2014-2015 Canonical Limited.
1202-#
1203-# This file is part of charm-helpers.
1204-#
1205-# charm-helpers is free software: you can redistribute it and/or modify
1206-# it under the terms of the GNU Lesser General Public License version 3 as
1207-# published by the Free Software Foundation.
1208-#
1209-# charm-helpers is distributed in the hope that it will be useful,
1210-# but WITHOUT ANY WARRANTY; without even the implied warranty of
1211-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1212-# GNU Lesser General Public License for more details.
1213-#
1214-# You should have received a copy of the GNU Lesser General Public License
1215-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1216-#
1217-#
1218-# Authors:
1219-# Kapil Thangavelu <kapil.foss@gmail.com>
1220-#
1221-"""
1222-Intro
1223------
1224-
1225-A simple way to store state in units. This provides a key value
1226-storage with support for versioned, transactional operation,
1227-and can calculate deltas from previous values to simplify unit logic
1228-when processing changes.
1229-
1230-
1231-Hook Integration
1232-----------------
1233-
1234-There are several extant frameworks for hook execution, including
1235-
1236- - charmhelpers.core.hookenv.Hooks
1237- - charmhelpers.core.services.ServiceManager
1238-
1239-The storage classes are framework agnostic, one simple integration is
1240-via the HookData contextmanager. It will record the current hook
1241-execution environment (including relation data, config data, etc.),
1242-setup a transaction and allow easy access to the changes from
1243-previously seen values. One consequence of the integration is the
1244-reservation of particular keys ('rels', 'unit', 'env', 'config',
1245-'charm_revisions') for their respective values.
1246-
1247-Here's a fully worked integration example using hookenv.Hooks::
1248-
1249- from charmhelper.core import hookenv, unitdata
1250-
1251- hook_data = unitdata.HookData()
1252- db = unitdata.kv()
1253- hooks = hookenv.Hooks()
1254-
1255- @hooks.hook
1256- def config_changed():
1257- # Print all changes to configuration from previously seen
1258- # values.
1259- for changed, (prev, cur) in hook_data.conf.items():
1260- print('config changed', changed,
1261- 'previous value', prev,
1262- 'current value', cur)
1263-
1264- # Get some unit specific bookeeping
1265- if not db.get('pkg_key'):
1266- key = urllib.urlopen('https://example.com/pkg_key').read()
1267- db.set('pkg_key', key)
1268-
1269- # Directly access all charm config as a mapping.
1270- conf = db.getrange('config', True)
1271-
1272- # Directly access all relation data as a mapping
1273- rels = db.getrange('rels', True)
1274-
1275- if __name__ == '__main__':
1276- with hook_data():
1277- hook.execute()
1278-
1279-
1280-A more basic integration is via the hook_scope context manager which simply
1281-manages transaction scope (and records hook name, and timestamp)::
1282-
1283- >>> from unitdata import kv
1284- >>> db = kv()
1285- >>> with db.hook_scope('install'):
1286- ... # do work, in transactional scope.
1287- ... db.set('x', 1)
1288- >>> db.get('x')
1289- 1
1290-
1291-
1292-Usage
1293------
1294-
1295-Values are automatically json de/serialized to preserve basic typing
1296-and complex data struct capabilities (dicts, lists, ints, booleans, etc).
1297-
1298-Individual values can be manipulated via get/set::
1299-
1300- >>> kv.set('y', True)
1301- >>> kv.get('y')
1302- True
1303-
1304- # We can set complex values (dicts, lists) as a single key.
1305- >>> kv.set('config', {'a': 1, 'b': True'})
1306-
1307- # Also supports returning dictionaries as a record which
1308- # provides attribute access.
1309- >>> config = kv.get('config', record=True)
1310- >>> config.b
1311- True
1312-
1313-
1314-Groups of keys can be manipulated with update/getrange::
1315-
1316- >>> kv.update({'z': 1, 'y': 2}, prefix="gui.")
1317- >>> kv.getrange('gui.', strip=True)
1318- {'z': 1, 'y': 2}
1319-
1320-When updating values, its very helpful to understand which values
1321-have actually changed and how have they changed. The storage
1322-provides a delta method to provide for this::
1323-
1324- >>> data = {'debug': True, 'option': 2}
1325- >>> delta = kv.delta(data, 'config.')
1326- >>> delta.debug.previous
1327- None
1328- >>> delta.debug.current
1329- True
1330- >>> delta
1331- {'debug': (None, True), 'option': (None, 2)}
1332-
1333-Note the delta method does not persist the actual change, it needs to
1334-be explicitly saved via 'update' method::
1335-
1336- >>> kv.update(data, 'config.')
1337-
1338-Values modified in the context of a hook scope retain historical values
1339-associated to the hookname.
1340-
1341- >>> with db.hook_scope('config-changed'):
1342- ... db.set('x', 42)
1343- >>> db.gethistory('x')
1344- [(1, u'x', 1, u'install', u'2015-01-21T16:49:30.038372'),
1345- (2, u'x', 42, u'config-changed', u'2015-01-21T16:49:30.038786')]
1346-
1347-"""
1348-
1349-import collections
1350-import contextlib
1351-import datetime
1352-import json
1353-import os
1354-import pprint
1355-import sqlite3
1356-import sys
1357-
1358-__author__ = 'Kapil Thangavelu <kapil.foss@gmail.com>'
1359-
1360-
1361-class Storage(object):
1362- """Simple key value database for local unit state within charms.
1363-
1364- Modifications are automatically committed at hook exit. That's
1365- currently regardless of exit code.
1366-
1367- To support dicts, lists, integer, floats, and booleans values
1368- are automatically json encoded/decoded.
1369- """
1370- def __init__(self, path=None):
1371- self.db_path = path
1372- if path is None:
1373- self.db_path = os.path.join(
1374- os.environ.get('CHARM_DIR', ''), '.unit-state.db')
1375- self.conn = sqlite3.connect('%s' % self.db_path)
1376- self.cursor = self.conn.cursor()
1377- self.revision = None
1378- self._closed = False
1379- self._init()
1380-
1381- def close(self):
1382- if self._closed:
1383- return
1384- self.flush(False)
1385- self.cursor.close()
1386- self.conn.close()
1387- self._closed = True
1388-
1389- def _scoped_query(self, stmt, params=None):
1390- if params is None:
1391- params = []
1392- return stmt, params
1393-
1394- def get(self, key, default=None, record=False):
1395- self.cursor.execute(
1396- *self._scoped_query(
1397- 'select data from kv where key=?', [key]))
1398- result = self.cursor.fetchone()
1399- if not result:
1400- return default
1401- if record:
1402- return Record(json.loads(result[0]))
1403- return json.loads(result[0])
1404-
1405- def getrange(self, key_prefix, strip=False):
1406- stmt = "select key, data from kv where key like '%s%%'" % key_prefix
1407- self.cursor.execute(*self._scoped_query(stmt))
1408- result = self.cursor.fetchall()
1409-
1410- if not result:
1411- return None
1412- if not strip:
1413- key_prefix = ''
1414- return dict([
1415- (k[len(key_prefix):], json.loads(v)) for k, v in result])
1416-
1417- def update(self, mapping, prefix=""):
1418- for k, v in mapping.items():
1419- self.set("%s%s" % (prefix, k), v)
1420-
1421- def unset(self, key):
1422- self.cursor.execute('delete from kv where key=?', [key])
1423- if self.revision and self.cursor.rowcount:
1424- self.cursor.execute(
1425- 'insert into kv_revisions values (?, ?, ?)',
1426- [key, self.revision, json.dumps('DELETED')])
1427-
1428- def set(self, key, value):
1429- serialized = json.dumps(value)
1430-
1431- self.cursor.execute(
1432- 'select data from kv where key=?', [key])
1433- exists = self.cursor.fetchone()
1434-
1435- # Skip mutations to the same value
1436- if exists:
1437- if exists[0] == serialized:
1438- return value
1439-
1440- if not exists:
1441- self.cursor.execute(
1442- 'insert into kv (key, data) values (?, ?)',
1443- (key, serialized))
1444- else:
1445- self.cursor.execute('''
1446- update kv
1447- set data = ?
1448- where key = ?''', [serialized, key])
1449-
1450- # Save
1451- if not self.revision:
1452- return value
1453-
1454- self.cursor.execute(
1455- 'select 1 from kv_revisions where key=? and revision=?',
1456- [key, self.revision])
1457- exists = self.cursor.fetchone()
1458-
1459- if not exists:
1460- self.cursor.execute(
1461- '''insert into kv_revisions (
1462- revision, key, data) values (?, ?, ?)''',
1463- (self.revision, key, serialized))
1464- else:
1465- self.cursor.execute(
1466- '''
1467- update kv_revisions
1468- set data = ?
1469- where key = ?
1470- and revision = ?''',
1471- [serialized, key, self.revision])
1472-
1473- return value
1474-
1475- def delta(self, mapping, prefix):
1476- """
1477- return a delta containing values that have changed.
1478- """
1479- previous = self.getrange(prefix, strip=True)
1480- if not previous:
1481- pk = set()
1482- else:
1483- pk = set(previous.keys())
1484- ck = set(mapping.keys())
1485- delta = DeltaSet()
1486-
1487- # added
1488- for k in ck.difference(pk):
1489- delta[k] = Delta(None, mapping[k])
1490-
1491- # removed
1492- for k in pk.difference(ck):
1493- delta[k] = Delta(previous[k], None)
1494-
1495- # changed
1496- for k in pk.intersection(ck):
1497- c = mapping[k]
1498- p = previous[k]
1499- if c != p:
1500- delta[k] = Delta(p, c)
1501-
1502- return delta
1503-
1504- @contextlib.contextmanager
1505- def hook_scope(self, name=""):
1506- """Scope all future interactions to the current hook execution
1507- revision."""
1508- assert not self.revision
1509- self.cursor.execute(
1510- 'insert into hooks (hook, date) values (?, ?)',
1511- (name or sys.argv[0],
1512- datetime.datetime.utcnow().isoformat()))
1513- self.revision = self.cursor.lastrowid
1514- try:
1515- yield self.revision
1516- self.revision = None
1517- except:
1518- self.flush(False)
1519- self.revision = None
1520- raise
1521- else:
1522- self.flush()
1523-
1524- def flush(self, save=True):
1525- if save:
1526- self.conn.commit()
1527- elif self._closed:
1528- return
1529- else:
1530- self.conn.rollback()
1531-
1532- def _init(self):
1533- self.cursor.execute('''
1534- create table if not exists kv (
1535- key text,
1536- data text,
1537- primary key (key)
1538- )''')
1539- self.cursor.execute('''
1540- create table if not exists kv_revisions (
1541- key text,
1542- revision integer,
1543- data text,
1544- primary key (key, revision)
1545- )''')
1546- self.cursor.execute('''
1547- create table if not exists hooks (
1548- version integer primary key autoincrement,
1549- hook text,
1550- date text
1551- )''')
1552- self.conn.commit()
1553-
1554- def gethistory(self, key, deserialize=False):
1555- self.cursor.execute(
1556- '''
1557- select kv.revision, kv.key, kv.data, h.hook, h.date
1558- from kv_revisions kv,
1559- hooks h
1560- where kv.key=?
1561- and kv.revision = h.version
1562- ''', [key])
1563- if deserialize is False:
1564- return self.cursor.fetchall()
1565- return map(_parse_history, self.cursor.fetchall())
1566-
1567- def debug(self, fh=sys.stderr):
1568- self.cursor.execute('select * from kv')
1569- pprint.pprint(self.cursor.fetchall(), stream=fh)
1570- self.cursor.execute('select * from kv_revisions')
1571- pprint.pprint(self.cursor.fetchall(), stream=fh)
1572-
1573-
1574-def _parse_history(d):
1575- return (d[0], d[1], json.loads(d[2]), d[3],
1576- datetime.datetime.strptime(d[-1], "%Y-%m-%dT%H:%M:%S.%f"))
1577-
1578-
1579-class HookData(object):
1580- """Simple integration for existing hook exec frameworks.
1581-
1582- Records all unit information, and stores deltas for processing
1583- by the hook.
1584-
1585- Sample::
1586-
1587- from charmhelper.core import hookenv, unitdata
1588-
1589- changes = unitdata.HookData()
1590- db = unitdata.kv()
1591- hooks = hookenv.Hooks()
1592-
1593- @hooks.hook
1594- def config_changed():
1595- # View all changes to configuration
1596- for changed, (prev, cur) in changes.conf.items():
1597- print('config changed', changed,
1598- 'previous value', prev,
1599- 'current value', cur)
1600-
1601- # Get some unit specific bookeeping
1602- if not db.get('pkg_key'):
1603- key = urllib.urlopen('https://example.com/pkg_key').read()
1604- db.set('pkg_key', key)
1605-
1606- if __name__ == '__main__':
1607- with changes():
1608- hook.execute()
1609-
1610- """
1611- def __init__(self):
1612- self.kv = kv()
1613- self.conf = None
1614- self.rels = None
1615-
1616- @contextlib.contextmanager
1617- def __call__(self):
1618- from charmhelpers.core import hookenv
1619- hook_name = hookenv.hook_name()
1620-
1621- with self.kv.hook_scope(hook_name):
1622- self._record_charm_version(hookenv.charm_dir())
1623- delta_config, delta_relation = self._record_hook(hookenv)
1624- yield self.kv, delta_config, delta_relation
1625-
1626- def _record_charm_version(self, charm_dir):
1627- # Record revisions.. charm revisions are meaningless
1628- # to charm authors as they don't control the revision.
1629- # so logic dependnent on revision is not particularly
1630- # useful, however it is useful for debugging analysis.
1631- charm_rev = open(
1632- os.path.join(charm_dir, 'revision')).read().strip()
1633- charm_rev = charm_rev or '0'
1634- revs = self.kv.get('charm_revisions', [])
1635- if charm_rev not in revs:
1636- revs.append(charm_rev.strip() or '0')
1637- self.kv.set('charm_revisions', revs)
1638-
1639- def _record_hook(self, hookenv):
1640- data = hookenv.execution_environment()
1641- self.conf = conf_delta = self.kv.delta(data['conf'], 'config')
1642- self.rels = rels_delta = self.kv.delta(data['rels'], 'rels')
1643- self.kv.set('env', data['env'])
1644- self.kv.set('unit', data['unit'])
1645- self.kv.set('relid', data.get('relid'))
1646- return conf_delta, rels_delta
1647-
1648-
1649-class Record(dict):
1650-
1651- __slots__ = ()
1652-
1653- def __getattr__(self, k):
1654- if k in self:
1655- return self[k]
1656- raise AttributeError(k)
1657-
1658-
1659-class DeltaSet(Record):
1660-
1661- __slots__ = ()
1662-
1663-
1664-Delta = collections.namedtuple('Delta', ['previous', 'current'])
1665-
1666-
1667-_KV = None
1668-
1669-
1670-def kv():
1671- global _KV
1672- if _KV is None:
1673- _KV = Storage()
1674- return _KV
1675
1676=== added symlink 'hooks/nrpe-external-master-relation-changed'
1677=== target is u'percona_hooks.py'
1678=== added symlink 'hooks/nrpe-external-master-relation-joined'
1679=== target is u'percona_hooks.py'
1680=== modified file 'hooks/percona_hooks.py'
1681--- hooks/percona_hooks.py 2015-02-16 14:12:42 +0000
1682+++ hooks/percona_hooks.py 2015-03-03 02:26:44 +0000
1683@@ -69,6 +69,8 @@
1684 is_address_in_network,
1685 )
1686
1687+from charmhelpers.contrib.charmsupport import nrpe
1688+
1689 hooks = Hooks()
1690
1691 LEADER_RES = 'grp_percona_cluster'
1692@@ -426,6 +428,23 @@
1693 relation_clear(r_id)
1694
1695
1696+@hooks.hook('nrpe-external-master-relation-joined',
1697+ 'nrpe-external-master-relation-changed')
1698+def update_nrpe_config():
1699+ # python-dbus is used by check_upstart_job
1700+ apt_install('python-dbus')
1701+ hostname = nrpe.get_nagios_hostname()
1702+ current_unit = nrpe.get_nagios_unit_name()
1703+ nrpe_setup = nrpe.NRPE(hostname=hostname)
1704+ nrpe.add_init_service_checks(nrpe_setup, 'mysql', current_unit)
1705+ nrpe_setup.add_check(
1706+ shortname='mysql_proc',
1707+ description='Check MySQL process {%s}' % current_unit,
1708+ check_cmd='check_procs -c 1:1 -C mysqld'
1709+ )
1710+ nrpe_setup.write()
1711+
1712+
1713 def main():
1714 try:
1715 hooks.execute(sys.argv)

Subscribers

People subscribed via source and target branches