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

Proposed by Brad Marshall
Status: Merged
Merged at revision: 106
Proposed branch: lp:~brad-marshall/charms/trusty/nova-compute/fix-nagios
Merge into: lp:~openstack-charmers-archive/charms/trusty/nova-compute/next
Diff against target: 1649 lines (+48/-902)
9 files modified
config.yaml (+6/-0)
hooks/charmhelpers/contrib/hahelpers/ceph.py (+0/-294)
hooks/charmhelpers/contrib/openstack/context.py (+28/-9)
hooks/charmhelpers/contrib/openstack/files/__init__.py (+0/-18)
hooks/charmhelpers/contrib/openstack/files/check_haproxy.sh (+0/-32)
hooks/charmhelpers/contrib/openstack/files/check_haproxy_queue_depth.sh (+0/-30)
hooks/charmhelpers/contrib/openstack/templates/zeromq (+14/-0)
hooks/charmhelpers/core/strutils.py (+0/-42)
hooks/charmhelpers/core/unitdata.py (+0/-477)
To merge this branch: bzr merge lp:~brad-marshall/charms/trusty/nova-compute/fix-nagios
Reviewer Review Type Date Requested Status
Liam Young (community) Approve
Review via email: mp+250703@code.launchpad.net

Description of the change

Synced charmhelpers, added nagios_servicegroup config option

To post a comment you must log in.
105. By Edward Hope-Morley

[trivial] charmhelpers sync

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

charm_lint_check #2218 nova-compute-next for brad-marshall mp250703
    LINT OK: passed

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

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

charm_unit_test #2007 nova-compute-next for brad-marshall mp250703
    UNIT FAIL: unit-test failed

UNIT Results (max last 2 lines):
  FAILED (SKIP=5, failures=1)
  make: *** [unit_test] Error 1

Full unit test output: http://paste.ubuntu.com/10396098/
Build: http://10.245.162.77:8080/job/charm_unit_test/2007/

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

charm_amulet_test #2164 nova-compute-next for brad-marshall mp250703
    AMULET FAIL: amulet-test failed

AMULET Results (max last 2 lines):
  ERROR subprocess encountered error code 1
  make: *** [test] Error 1

Full amulet test output: http://paste.ubuntu.com/10396800/
Build: http://10.245.162.77:8080/job/charm_amulet_test/2164/

106. By Brad Marshall

[bradm] Fixed text conflicts

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 'config.yaml'
2--- config.yaml 2015-02-16 09:26:23 +0000
3+++ config.yaml 2015-02-26 00:02:34 +0000
4@@ -122,6 +122,12 @@
5 juju-myservice-0
6 If you're running multiple environments with the same services in them
7 this allows you to differentiate between them.
8+ nagios_servicegroups:
9+ default: ""
10+ type: string
11+ description: |
12+ A comma-separated list of nagios servicegroups.
13+ If left empty, the nagios_context will be used as the servicegroup
14 disable-neutron-security-groups:
15 type: boolean
16 default: False
17
18=== removed file 'hooks/charmhelpers/contrib/hahelpers/ceph.py'
19--- hooks/charmhelpers/contrib/hahelpers/ceph.py 2013-09-20 16:40:54 +0000
20+++ hooks/charmhelpers/contrib/hahelpers/ceph.py 1970-01-01 00:00:00 +0000
21@@ -1,294 +0,0 @@
22-#
23-# Copyright 2012 Canonical Ltd.
24-#
25-# This file is sourced from lp:openstack-charm-helpers
26-#
27-# Authors:
28-# James Page <james.page@ubuntu.com>
29-# Adam Gandelman <adamg@ubuntu.com>
30-#
31-
32-import commands
33-import os
34-import shutil
35-import time
36-
37-from subprocess import (
38- check_call,
39- check_output,
40- CalledProcessError
41-)
42-
43-from charmhelpers.core.hookenv import (
44- relation_get,
45- relation_ids,
46- related_units,
47- log,
48- INFO,
49- ERROR
50-)
51-
52-from charmhelpers.fetch import (
53- apt_install,
54-)
55-
56-from charmhelpers.core.host import (
57- mount,
58- mounts,
59- service_start,
60- service_stop,
61- umount,
62-)
63-
64-KEYRING = '/etc/ceph/ceph.client.%s.keyring'
65-KEYFILE = '/etc/ceph/ceph.client.%s.key'
66-
67-CEPH_CONF = """[global]
68- auth supported = %(auth)s
69- keyring = %(keyring)s
70- mon host = %(mon_hosts)s
71-"""
72-
73-
74-def running(service):
75- # this local util can be dropped as soon the following branch lands
76- # in lp:charm-helpers
77- # https://code.launchpad.net/~gandelman-a/charm-helpers/service_running/
78- try:
79- output = check_output(['service', service, 'status'])
80- except CalledProcessError:
81- return False
82- else:
83- if ("start/running" in output or "is running" in output):
84- return True
85- else:
86- return False
87-
88-
89-def install():
90- ceph_dir = "/etc/ceph"
91- if not os.path.isdir(ceph_dir):
92- os.mkdir(ceph_dir)
93- apt_install('ceph-common', fatal=True)
94-
95-
96-def rbd_exists(service, pool, rbd_img):
97- (rc, out) = commands.getstatusoutput('rbd list --id %s --pool %s' %
98- (service, pool))
99- return rbd_img in out
100-
101-
102-def create_rbd_image(service, pool, image, sizemb):
103- cmd = [
104- 'rbd',
105- 'create',
106- image,
107- '--size',
108- str(sizemb),
109- '--id',
110- service,
111- '--pool',
112- pool
113- ]
114- check_call(cmd)
115-
116-
117-def pool_exists(service, name):
118- (rc, out) = commands.getstatusoutput("rados --id %s lspools" % service)
119- return name in out
120-
121-
122-def create_pool(service, name):
123- cmd = [
124- 'rados',
125- '--id',
126- service,
127- 'mkpool',
128- name
129- ]
130- check_call(cmd)
131-
132-
133-def keyfile_path(service):
134- return KEYFILE % service
135-
136-
137-def keyring_path(service):
138- return KEYRING % service
139-
140-
141-def create_keyring(service, key):
142- keyring = keyring_path(service)
143- if os.path.exists(keyring):
144- log('ceph: Keyring exists at %s.' % keyring, level=INFO)
145- cmd = [
146- 'ceph-authtool',
147- keyring,
148- '--create-keyring',
149- '--name=client.%s' % service,
150- '--add-key=%s' % key
151- ]
152- check_call(cmd)
153- log('ceph: Created new ring at %s.' % keyring, level=INFO)
154-
155-
156-def create_key_file(service, key):
157- # create a file containing the key
158- keyfile = keyfile_path(service)
159- if os.path.exists(keyfile):
160- log('ceph: Keyfile exists at %s.' % keyfile, level=INFO)
161- fd = open(keyfile, 'w')
162- fd.write(key)
163- fd.close()
164- log('ceph: Created new keyfile at %s.' % keyfile, level=INFO)
165-
166-
167-def get_ceph_nodes():
168- hosts = []
169- for r_id in relation_ids('ceph'):
170- for unit in related_units(r_id):
171- hosts.append(relation_get('private-address', unit=unit, rid=r_id))
172- return hosts
173-
174-
175-def configure(service, key, auth):
176- create_keyring(service, key)
177- create_key_file(service, key)
178- hosts = get_ceph_nodes()
179- mon_hosts = ",".join(map(str, hosts))
180- keyring = keyring_path(service)
181- with open('/etc/ceph/ceph.conf', 'w') as ceph_conf:
182- ceph_conf.write(CEPH_CONF % locals())
183- modprobe_kernel_module('rbd')
184-
185-
186-def image_mapped(image_name):
187- (rc, out) = commands.getstatusoutput('rbd showmapped')
188- return image_name in out
189-
190-
191-def map_block_storage(service, pool, image):
192- cmd = [
193- 'rbd',
194- 'map',
195- '%s/%s' % (pool, image),
196- '--user',
197- service,
198- '--secret',
199- keyfile_path(service),
200- ]
201- check_call(cmd)
202-
203-
204-def filesystem_mounted(fs):
205- return fs in [f for m, f in mounts()]
206-
207-
208-def make_filesystem(blk_device, fstype='ext4', timeout=10):
209- count = 0
210- e_noent = os.errno.ENOENT
211- while not os.path.exists(blk_device):
212- if count >= timeout:
213- log('ceph: gave up waiting on block device %s' % blk_device,
214- level=ERROR)
215- raise IOError(e_noent, os.strerror(e_noent), blk_device)
216- log('ceph: waiting for block device %s to appear' % blk_device,
217- level=INFO)
218- count += 1
219- time.sleep(1)
220- else:
221- log('ceph: Formatting block device %s as filesystem %s.' %
222- (blk_device, fstype), level=INFO)
223- check_call(['mkfs', '-t', fstype, blk_device])
224-
225-
226-def place_data_on_ceph(service, blk_device, data_src_dst, fstype='ext4'):
227- # mount block device into /mnt
228- mount(blk_device, '/mnt')
229-
230- # copy data to /mnt
231- try:
232- copy_files(data_src_dst, '/mnt')
233- except:
234- pass
235-
236- # umount block device
237- umount('/mnt')
238-
239- _dir = os.stat(data_src_dst)
240- uid = _dir.st_uid
241- gid = _dir.st_gid
242-
243- # re-mount where the data should originally be
244- mount(blk_device, data_src_dst, persist=True)
245-
246- # ensure original ownership of new mount.
247- cmd = ['chown', '-R', '%s:%s' % (uid, gid), data_src_dst]
248- check_call(cmd)
249-
250-
251-# TODO: re-use
252-def modprobe_kernel_module(module):
253- log('ceph: Loading kernel module', level=INFO)
254- cmd = ['modprobe', module]
255- check_call(cmd)
256- cmd = 'echo %s >> /etc/modules' % module
257- check_call(cmd, shell=True)
258-
259-
260-def copy_files(src, dst, symlinks=False, ignore=None):
261- for item in os.listdir(src):
262- s = os.path.join(src, item)
263- d = os.path.join(dst, item)
264- if os.path.isdir(s):
265- shutil.copytree(s, d, symlinks, ignore)
266- else:
267- shutil.copy2(s, d)
268-
269-
270-def ensure_ceph_storage(service, pool, rbd_img, sizemb, mount_point,
271- blk_device, fstype, system_services=[]):
272- """
273- To be called from the current cluster leader.
274- Ensures given pool and RBD image exists, is mapped to a block device,
275- and the device is formatted and mounted at the given mount_point.
276-
277- If formatting a device for the first time, data existing at mount_point
278- will be migrated to the RBD device before being remounted.
279-
280- All services listed in system_services will be stopped prior to data
281- migration and restarted when complete.
282- """
283- # Ensure pool, RBD image, RBD mappings are in place.
284- if not pool_exists(service, pool):
285- log('ceph: Creating new pool %s.' % pool, level=INFO)
286- create_pool(service, pool)
287-
288- if not rbd_exists(service, pool, rbd_img):
289- log('ceph: Creating RBD image (%s).' % rbd_img, level=INFO)
290- create_rbd_image(service, pool, rbd_img, sizemb)
291-
292- if not image_mapped(rbd_img):
293- log('ceph: Mapping RBD Image as a Block Device.', level=INFO)
294- map_block_storage(service, pool, rbd_img)
295-
296- # make file system
297- # TODO: What happens if for whatever reason this is run again and
298- # the data is already in the rbd device and/or is mounted??
299- # When it is mounted already, it will fail to make the fs
300- # XXX: This is really sketchy! Need to at least add an fstab entry
301- # otherwise this hook will blow away existing data if its executed
302- # after a reboot.
303- if not filesystem_mounted(mount_point):
304- make_filesystem(blk_device, fstype)
305-
306- for svc in system_services:
307- if running(svc):
308- log('Stopping services %s prior to migrating data.' % svc,
309- level=INFO)
310- service_stop(svc)
311-
312- place_data_on_ceph(service, blk_device, mount_point, fstype)
313-
314- for svc in system_services:
315- service_start(svc)
316
317=== modified file 'hooks/charmhelpers/contrib/openstack/context.py'
318--- hooks/charmhelpers/contrib/openstack/context.py 2015-01-26 09:46:57 +0000
319+++ hooks/charmhelpers/contrib/openstack/context.py 2015-02-26 00:02:34 +0000
320@@ -279,9 +279,25 @@
321 class IdentityServiceContext(OSContextGenerator):
322 interfaces = ['identity-service']
323
324+ def __init__(self, service=None, service_user=None):
325+ self.service = service
326+ self.service_user = service_user
327+
328 def __call__(self):
329 log('Generating template context for identity-service', level=DEBUG)
330 ctxt = {}
331+
332+ if self.service and self.service_user:
333+ # This is required for pki token signing if we don't want /tmp to
334+ # be used.
335+ cachedir = '/var/cache/%s' % (self.service)
336+ if not os.path.isdir(cachedir):
337+ log("Creating service cache dir %s" % (cachedir), level=DEBUG)
338+ mkdir(path=cachedir, owner=self.service_user,
339+ group=self.service_user, perms=0o700)
340+
341+ ctxt['signing_dir'] = cachedir
342+
343 for rid in relation_ids('identity-service'):
344 for unit in related_units(rid):
345 rdata = relation_get(rid=rid, unit=unit)
346@@ -291,15 +307,16 @@
347 auth_host = format_ipv6_addr(auth_host) or auth_host
348 svc_protocol = rdata.get('service_protocol') or 'http'
349 auth_protocol = rdata.get('auth_protocol') or 'http'
350- ctxt = {'service_port': rdata.get('service_port'),
351- 'service_host': serv_host,
352- 'auth_host': auth_host,
353- 'auth_port': rdata.get('auth_port'),
354- 'admin_tenant_name': rdata.get('service_tenant'),
355- 'admin_user': rdata.get('service_username'),
356- 'admin_password': rdata.get('service_password'),
357- 'service_protocol': svc_protocol,
358- 'auth_protocol': auth_protocol}
359+ ctxt.update({'service_port': rdata.get('service_port'),
360+ 'service_host': serv_host,
361+ 'auth_host': auth_host,
362+ 'auth_port': rdata.get('auth_port'),
363+ 'admin_tenant_name': rdata.get('service_tenant'),
364+ 'admin_user': rdata.get('service_username'),
365+ 'admin_password': rdata.get('service_password'),
366+ 'service_protocol': svc_protocol,
367+ 'auth_protocol': auth_protocol})
368+
369 if context_complete(ctxt):
370 # NOTE(jamespage) this is required for >= icehouse
371 # so a missing value just indicates keystone needs
372@@ -1021,6 +1038,8 @@
373 for unit in related_units(rid):
374 ctxt['zmq_nonce'] = relation_get('nonce', unit, rid)
375 ctxt['zmq_host'] = relation_get('host', unit, rid)
376+ ctxt['zmq_redis_address'] = relation_get(
377+ 'zmq_redis_address', unit, rid)
378
379 return ctxt
380
381
382=== added directory 'hooks/charmhelpers/contrib/openstack/files'
383=== removed directory 'hooks/charmhelpers/contrib/openstack/files'
384=== added file 'hooks/charmhelpers/contrib/openstack/files/__init__.py'
385--- hooks/charmhelpers/contrib/openstack/files/__init__.py 1970-01-01 00:00:00 +0000
386+++ hooks/charmhelpers/contrib/openstack/files/__init__.py 2015-02-26 00:02:34 +0000
387@@ -0,0 +1,18 @@
388+# Copyright 2014-2015 Canonical Limited.
389+#
390+# This file is part of charm-helpers.
391+#
392+# charm-helpers is free software: you can redistribute it and/or modify
393+# it under the terms of the GNU Lesser General Public License version 3 as
394+# published by the Free Software Foundation.
395+#
396+# charm-helpers is distributed in the hope that it will be useful,
397+# but WITHOUT ANY WARRANTY; without even the implied warranty of
398+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
399+# GNU Lesser General Public License for more details.
400+#
401+# You should have received a copy of the GNU Lesser General Public License
402+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
403+
404+# dummy __init__.py to fool syncer into thinking this is a syncable python
405+# module
406
407=== removed file 'hooks/charmhelpers/contrib/openstack/files/__init__.py'
408--- hooks/charmhelpers/contrib/openstack/files/__init__.py 2015-02-24 11:07:48 +0000
409+++ hooks/charmhelpers/contrib/openstack/files/__init__.py 1970-01-01 00:00:00 +0000
410@@ -1,18 +0,0 @@
411-# Copyright 2014-2015 Canonical Limited.
412-#
413-# This file is part of charm-helpers.
414-#
415-# charm-helpers is free software: you can redistribute it and/or modify
416-# it under the terms of the GNU Lesser General Public License version 3 as
417-# published by the Free Software Foundation.
418-#
419-# charm-helpers is distributed in the hope that it will be useful,
420-# but WITHOUT ANY WARRANTY; without even the implied warranty of
421-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
422-# GNU Lesser General Public License for more details.
423-#
424-# You should have received a copy of the GNU Lesser General Public License
425-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
426-
427-# dummy __init__.py to fool syncer into thinking this is a syncable python
428-# module
429
430=== added file 'hooks/charmhelpers/contrib/openstack/files/check_haproxy.sh'
431--- hooks/charmhelpers/contrib/openstack/files/check_haproxy.sh 1970-01-01 00:00:00 +0000
432+++ hooks/charmhelpers/contrib/openstack/files/check_haproxy.sh 2015-02-26 00:02:34 +0000
433@@ -0,0 +1,32 @@
434+#!/bin/bash
435+#--------------------------------------------
436+# This file is managed by Juju
437+#--------------------------------------------
438+#
439+# Copyright 2009,2012 Canonical Ltd.
440+# Author: Tom Haddon
441+
442+CRITICAL=0
443+NOTACTIVE=''
444+LOGFILE=/var/log/nagios/check_haproxy.log
445+AUTH=$(grep -r "stats auth" /etc/haproxy | head -1 | awk '{print $4}')
446+
447+for appserver in $(grep ' server' /etc/haproxy/haproxy.cfg | awk '{print $2'});
448+do
449+ output=$(/usr/lib/nagios/plugins/check_http -a ${AUTH} -I 127.0.0.1 -p 8888 --regex="class=\"(active|backup)(2|3).*${appserver}" -e ' 200 OK')
450+ if [ $? != 0 ]; then
451+ date >> $LOGFILE
452+ echo $output >> $LOGFILE
453+ /usr/lib/nagios/plugins/check_http -a ${AUTH} -I 127.0.0.1 -p 8888 -v | grep $appserver >> $LOGFILE 2>&1
454+ CRITICAL=1
455+ NOTACTIVE="${NOTACTIVE} $appserver"
456+ fi
457+done
458+
459+if [ $CRITICAL = 1 ]; then
460+ echo "CRITICAL:${NOTACTIVE}"
461+ exit 2
462+fi
463+
464+echo "OK: All haproxy instances looking good"
465+exit 0
466
467=== removed file 'hooks/charmhelpers/contrib/openstack/files/check_haproxy.sh'
468--- hooks/charmhelpers/contrib/openstack/files/check_haproxy.sh 2015-02-24 11:07:48 +0000
469+++ hooks/charmhelpers/contrib/openstack/files/check_haproxy.sh 1970-01-01 00:00:00 +0000
470@@ -1,32 +0,0 @@
471-#!/bin/bash
472-#--------------------------------------------
473-# This file is managed by Juju
474-#--------------------------------------------
475-#
476-# Copyright 2009,2012 Canonical Ltd.
477-# Author: Tom Haddon
478-
479-CRITICAL=0
480-NOTACTIVE=''
481-LOGFILE=/var/log/nagios/check_haproxy.log
482-AUTH=$(grep -r "stats auth" /etc/haproxy | head -1 | awk '{print $4}')
483-
484-for appserver in $(grep ' server' /etc/haproxy/haproxy.cfg | awk '{print $2'});
485-do
486- output=$(/usr/lib/nagios/plugins/check_http -a ${AUTH} -I 127.0.0.1 -p 8888 --regex="class=\"(active|backup)(2|3).*${appserver}" -e ' 200 OK')
487- if [ $? != 0 ]; then
488- date >> $LOGFILE
489- echo $output >> $LOGFILE
490- /usr/lib/nagios/plugins/check_http -a ${AUTH} -I 127.0.0.1 -p 8888 -v | grep $appserver >> $LOGFILE 2>&1
491- CRITICAL=1
492- NOTACTIVE="${NOTACTIVE} $appserver"
493- fi
494-done
495-
496-if [ $CRITICAL = 1 ]; then
497- echo "CRITICAL:${NOTACTIVE}"
498- exit 2
499-fi
500-
501-echo "OK: All haproxy instances looking good"
502-exit 0
503
504=== added file 'hooks/charmhelpers/contrib/openstack/files/check_haproxy_queue_depth.sh'
505--- hooks/charmhelpers/contrib/openstack/files/check_haproxy_queue_depth.sh 1970-01-01 00:00:00 +0000
506+++ hooks/charmhelpers/contrib/openstack/files/check_haproxy_queue_depth.sh 2015-02-26 00:02:34 +0000
507@@ -0,0 +1,30 @@
508+#!/bin/bash
509+#--------------------------------------------
510+# This file is managed by Juju
511+#--------------------------------------------
512+#
513+# Copyright 2009,2012 Canonical Ltd.
514+# Author: Tom Haddon
515+
516+# These should be config options at some stage
517+CURRQthrsh=0
518+MAXQthrsh=100
519+
520+AUTH=$(grep -r "stats auth" /etc/haproxy | head -1 | awk '{print $4}')
521+
522+HAPROXYSTATS=$(/usr/lib/nagios/plugins/check_http -a ${AUTH} -I 127.0.0.1 -p 8888 -u '/;csv' -v)
523+
524+for BACKEND in $(echo $HAPROXYSTATS| xargs -n1 | grep BACKEND | awk -F , '{print $1}')
525+do
526+ CURRQ=$(echo "$HAPROXYSTATS" | grep $BACKEND | grep BACKEND | cut -d , -f 3)
527+ MAXQ=$(echo "$HAPROXYSTATS" | grep $BACKEND | grep BACKEND | cut -d , -f 4)
528+
529+ if [[ $CURRQ -gt $CURRQthrsh || $MAXQ -gt $MAXQthrsh ]] ; then
530+ echo "CRITICAL: queue depth for $BACKEND - CURRENT:$CURRQ MAX:$MAXQ"
531+ exit 2
532+ fi
533+done
534+
535+echo "OK: All haproxy queue depths looking good"
536+exit 0
537+
538
539=== removed file 'hooks/charmhelpers/contrib/openstack/files/check_haproxy_queue_depth.sh'
540--- hooks/charmhelpers/contrib/openstack/files/check_haproxy_queue_depth.sh 2015-02-24 11:07:48 +0000
541+++ hooks/charmhelpers/contrib/openstack/files/check_haproxy_queue_depth.sh 1970-01-01 00:00:00 +0000
542@@ -1,30 +0,0 @@
543-#!/bin/bash
544-#--------------------------------------------
545-# This file is managed by Juju
546-#--------------------------------------------
547-#
548-# Copyright 2009,2012 Canonical Ltd.
549-# Author: Tom Haddon
550-
551-# These should be config options at some stage
552-CURRQthrsh=0
553-MAXQthrsh=100
554-
555-AUTH=$(grep -r "stats auth" /etc/haproxy | head -1 | awk '{print $4}')
556-
557-HAPROXYSTATS=$(/usr/lib/nagios/plugins/check_http -a ${AUTH} -I 127.0.0.1 -p 8888 -u '/;csv' -v)
558-
559-for BACKEND in $(echo $HAPROXYSTATS| xargs -n1 | grep BACKEND | awk -F , '{print $1}')
560-do
561- CURRQ=$(echo "$HAPROXYSTATS" | grep $BACKEND | grep BACKEND | cut -d , -f 3)
562- MAXQ=$(echo "$HAPROXYSTATS" | grep $BACKEND | grep BACKEND | cut -d , -f 4)
563-
564- if [[ $CURRQ -gt $CURRQthrsh || $MAXQ -gt $MAXQthrsh ]] ; then
565- echo "CRITICAL: queue depth for $BACKEND - CURRENT:$CURRQ MAX:$MAXQ"
566- exit 2
567- fi
568-done
569-
570-echo "OK: All haproxy queue depths looking good"
571-exit 0
572-
573
574=== added file 'hooks/charmhelpers/contrib/openstack/templates/zeromq'
575--- hooks/charmhelpers/contrib/openstack/templates/zeromq 1970-01-01 00:00:00 +0000
576+++ hooks/charmhelpers/contrib/openstack/templates/zeromq 2015-02-26 00:02:34 +0000
577@@ -0,0 +1,14 @@
578+{% if zmq_host -%}
579+# ZeroMQ configuration (restart-nonce: {{ zmq_nonce }})
580+rpc_backend = zmq
581+rpc_zmq_host = {{ zmq_host }}
582+{% if zmq_redis_address -%}
583+rpc_zmq_matchmaker = oslo.messaging._drivers.matchmaker_redis.MatchMakerRedis
584+matchmaker_heartbeat_freq = 15
585+matchmaker_heartbeat_ttl = 30
586+[matchmaker_redis]
587+host = {{ zmq_redis_address }}
588+{% else -%}
589+rpc_zmq_matchmaker = oslo.messaging._drivers.matchmaker_ring.MatchMakerRing
590+{% endif -%}
591+{% endif -%}
592
593=== added file 'hooks/charmhelpers/core/strutils.py'
594--- hooks/charmhelpers/core/strutils.py 1970-01-01 00:00:00 +0000
595+++ hooks/charmhelpers/core/strutils.py 2015-02-26 00:02:34 +0000
596@@ -0,0 +1,42 @@
597+#!/usr/bin/env python
598+# -*- coding: utf-8 -*-
599+
600+# Copyright 2014-2015 Canonical Limited.
601+#
602+# This file is part of charm-helpers.
603+#
604+# charm-helpers is free software: you can redistribute it and/or modify
605+# it under the terms of the GNU Lesser General Public License version 3 as
606+# published by the Free Software Foundation.
607+#
608+# charm-helpers is distributed in the hope that it will be useful,
609+# but WITHOUT ANY WARRANTY; without even the implied warranty of
610+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
611+# GNU Lesser General Public License for more details.
612+#
613+# You should have received a copy of the GNU Lesser General Public License
614+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
615+
616+import six
617+
618+
619+def bool_from_string(value):
620+ """Interpret string value as boolean.
621+
622+ Returns True if value translates to True otherwise False.
623+ """
624+ if isinstance(value, six.string_types):
625+ value = six.text_type(value)
626+ else:
627+ msg = "Unable to interpret non-string value '%s' as boolean" % (value)
628+ raise ValueError(msg)
629+
630+ value = value.strip().lower()
631+
632+ if value in ['y', 'yes', 'true', 't']:
633+ return True
634+ elif value in ['n', 'no', 'false', 'f']:
635+ return False
636+
637+ msg = "Unable to interpret string value '%s' as boolean" % (value)
638+ raise ValueError(msg)
639
640=== removed file 'hooks/charmhelpers/core/strutils.py'
641--- hooks/charmhelpers/core/strutils.py 2015-02-24 11:07:48 +0000
642+++ hooks/charmhelpers/core/strutils.py 1970-01-01 00:00:00 +0000
643@@ -1,42 +0,0 @@
644-#!/usr/bin/env python
645-# -*- coding: utf-8 -*-
646-
647-# Copyright 2014-2015 Canonical Limited.
648-#
649-# This file is part of charm-helpers.
650-#
651-# charm-helpers is free software: you can redistribute it and/or modify
652-# it under the terms of the GNU Lesser General Public License version 3 as
653-# published by the Free Software Foundation.
654-#
655-# charm-helpers is distributed in the hope that it will be useful,
656-# but WITHOUT ANY WARRANTY; without even the implied warranty of
657-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
658-# GNU Lesser General Public License for more details.
659-#
660-# You should have received a copy of the GNU Lesser General Public License
661-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
662-
663-import six
664-
665-
666-def bool_from_string(value):
667- """Interpret string value as boolean.
668-
669- Returns True if value translates to True otherwise False.
670- """
671- if isinstance(value, six.string_types):
672- value = six.text_type(value)
673- else:
674- msg = "Unable to interpret non-string value '%s' as boolean" % (value)
675- raise ValueError(msg)
676-
677- value = value.strip().lower()
678-
679- if value in ['y', 'yes', 'true', 't']:
680- return True
681- elif value in ['n', 'no', 'false', 'f']:
682- return False
683-
684- msg = "Unable to interpret string value '%s' as boolean" % (value)
685- raise ValueError(msg)
686
687=== added file 'hooks/charmhelpers/core/unitdata.py'
688--- hooks/charmhelpers/core/unitdata.py 1970-01-01 00:00:00 +0000
689+++ hooks/charmhelpers/core/unitdata.py 2015-02-26 00:02:34 +0000
690@@ -0,0 +1,477 @@
691+#!/usr/bin/env python
692+# -*- coding: utf-8 -*-
693+#
694+# Copyright 2014-2015 Canonical Limited.
695+#
696+# This file is part of charm-helpers.
697+#
698+# charm-helpers is free software: you can redistribute it and/or modify
699+# it under the terms of the GNU Lesser General Public License version 3 as
700+# published by the Free Software Foundation.
701+#
702+# charm-helpers is distributed in the hope that it will be useful,
703+# but WITHOUT ANY WARRANTY; without even the implied warranty of
704+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
705+# GNU Lesser General Public License for more details.
706+#
707+# You should have received a copy of the GNU Lesser General Public License
708+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
709+#
710+#
711+# Authors:
712+# Kapil Thangavelu <kapil.foss@gmail.com>
713+#
714+"""
715+Intro
716+-----
717+
718+A simple way to store state in units. This provides a key value
719+storage with support for versioned, transactional operation,
720+and can calculate deltas from previous values to simplify unit logic
721+when processing changes.
722+
723+
724+Hook Integration
725+----------------
726+
727+There are several extant frameworks for hook execution, including
728+
729+ - charmhelpers.core.hookenv.Hooks
730+ - charmhelpers.core.services.ServiceManager
731+
732+The storage classes are framework agnostic, one simple integration is
733+via the HookData contextmanager. It will record the current hook
734+execution environment (including relation data, config data, etc.),
735+setup a transaction and allow easy access to the changes from
736+previously seen values. One consequence of the integration is the
737+reservation of particular keys ('rels', 'unit', 'env', 'config',
738+'charm_revisions') for their respective values.
739+
740+Here's a fully worked integration example using hookenv.Hooks::
741+
742+ from charmhelper.core import hookenv, unitdata
743+
744+ hook_data = unitdata.HookData()
745+ db = unitdata.kv()
746+ hooks = hookenv.Hooks()
747+
748+ @hooks.hook
749+ def config_changed():
750+ # Print all changes to configuration from previously seen
751+ # values.
752+ for changed, (prev, cur) in hook_data.conf.items():
753+ print('config changed', changed,
754+ 'previous value', prev,
755+ 'current value', cur)
756+
757+ # Get some unit specific bookeeping
758+ if not db.get('pkg_key'):
759+ key = urllib.urlopen('https://example.com/pkg_key').read()
760+ db.set('pkg_key', key)
761+
762+ # Directly access all charm config as a mapping.
763+ conf = db.getrange('config', True)
764+
765+ # Directly access all relation data as a mapping
766+ rels = db.getrange('rels', True)
767+
768+ if __name__ == '__main__':
769+ with hook_data():
770+ hook.execute()
771+
772+
773+A more basic integration is via the hook_scope context manager which simply
774+manages transaction scope (and records hook name, and timestamp)::
775+
776+ >>> from unitdata import kv
777+ >>> db = kv()
778+ >>> with db.hook_scope('install'):
779+ ... # do work, in transactional scope.
780+ ... db.set('x', 1)
781+ >>> db.get('x')
782+ 1
783+
784+
785+Usage
786+-----
787+
788+Values are automatically json de/serialized to preserve basic typing
789+and complex data struct capabilities (dicts, lists, ints, booleans, etc).
790+
791+Individual values can be manipulated via get/set::
792+
793+ >>> kv.set('y', True)
794+ >>> kv.get('y')
795+ True
796+
797+ # We can set complex values (dicts, lists) as a single key.
798+ >>> kv.set('config', {'a': 1, 'b': True'})
799+
800+ # Also supports returning dictionaries as a record which
801+ # provides attribute access.
802+ >>> config = kv.get('config', record=True)
803+ >>> config.b
804+ True
805+
806+
807+Groups of keys can be manipulated with update/getrange::
808+
809+ >>> kv.update({'z': 1, 'y': 2}, prefix="gui.")
810+ >>> kv.getrange('gui.', strip=True)
811+ {'z': 1, 'y': 2}
812+
813+When updating values, its very helpful to understand which values
814+have actually changed and how have they changed. The storage
815+provides a delta method to provide for this::
816+
817+ >>> data = {'debug': True, 'option': 2}
818+ >>> delta = kv.delta(data, 'config.')
819+ >>> delta.debug.previous
820+ None
821+ >>> delta.debug.current
822+ True
823+ >>> delta
824+ {'debug': (None, True), 'option': (None, 2)}
825+
826+Note the delta method does not persist the actual change, it needs to
827+be explicitly saved via 'update' method::
828+
829+ >>> kv.update(data, 'config.')
830+
831+Values modified in the context of a hook scope retain historical values
832+associated to the hookname.
833+
834+ >>> with db.hook_scope('config-changed'):
835+ ... db.set('x', 42)
836+ >>> db.gethistory('x')
837+ [(1, u'x', 1, u'install', u'2015-01-21T16:49:30.038372'),
838+ (2, u'x', 42, u'config-changed', u'2015-01-21T16:49:30.038786')]
839+
840+"""
841+
842+import collections
843+import contextlib
844+import datetime
845+import json
846+import os
847+import pprint
848+import sqlite3
849+import sys
850+
851+__author__ = 'Kapil Thangavelu <kapil.foss@gmail.com>'
852+
853+
854+class Storage(object):
855+ """Simple key value database for local unit state within charms.
856+
857+ Modifications are automatically committed at hook exit. That's
858+ currently regardless of exit code.
859+
860+ To support dicts, lists, integer, floats, and booleans values
861+ are automatically json encoded/decoded.
862+ """
863+ def __init__(self, path=None):
864+ self.db_path = path
865+ if path is None:
866+ self.db_path = os.path.join(
867+ os.environ.get('CHARM_DIR', ''), '.unit-state.db')
868+ self.conn = sqlite3.connect('%s' % self.db_path)
869+ self.cursor = self.conn.cursor()
870+ self.revision = None
871+ self._closed = False
872+ self._init()
873+
874+ def close(self):
875+ if self._closed:
876+ return
877+ self.flush(False)
878+ self.cursor.close()
879+ self.conn.close()
880+ self._closed = True
881+
882+ def _scoped_query(self, stmt, params=None):
883+ if params is None:
884+ params = []
885+ return stmt, params
886+
887+ def get(self, key, default=None, record=False):
888+ self.cursor.execute(
889+ *self._scoped_query(
890+ 'select data from kv where key=?', [key]))
891+ result = self.cursor.fetchone()
892+ if not result:
893+ return default
894+ if record:
895+ return Record(json.loads(result[0]))
896+ return json.loads(result[0])
897+
898+ def getrange(self, key_prefix, strip=False):
899+ stmt = "select key, data from kv where key like '%s%%'" % key_prefix
900+ self.cursor.execute(*self._scoped_query(stmt))
901+ result = self.cursor.fetchall()
902+
903+ if not result:
904+ return None
905+ if not strip:
906+ key_prefix = ''
907+ return dict([
908+ (k[len(key_prefix):], json.loads(v)) for k, v in result])
909+
910+ def update(self, mapping, prefix=""):
911+ for k, v in mapping.items():
912+ self.set("%s%s" % (prefix, k), v)
913+
914+ def unset(self, key):
915+ self.cursor.execute('delete from kv where key=?', [key])
916+ if self.revision and self.cursor.rowcount:
917+ self.cursor.execute(
918+ 'insert into kv_revisions values (?, ?, ?)',
919+ [key, self.revision, json.dumps('DELETED')])
920+
921+ def set(self, key, value):
922+ serialized = json.dumps(value)
923+
924+ self.cursor.execute(
925+ 'select data from kv where key=?', [key])
926+ exists = self.cursor.fetchone()
927+
928+ # Skip mutations to the same value
929+ if exists:
930+ if exists[0] == serialized:
931+ return value
932+
933+ if not exists:
934+ self.cursor.execute(
935+ 'insert into kv (key, data) values (?, ?)',
936+ (key, serialized))
937+ else:
938+ self.cursor.execute('''
939+ update kv
940+ set data = ?
941+ where key = ?''', [serialized, key])
942+
943+ # Save
944+ if not self.revision:
945+ return value
946+
947+ self.cursor.execute(
948+ 'select 1 from kv_revisions where key=? and revision=?',
949+ [key, self.revision])
950+ exists = self.cursor.fetchone()
951+
952+ if not exists:
953+ self.cursor.execute(
954+ '''insert into kv_revisions (
955+ revision, key, data) values (?, ?, ?)''',
956+ (self.revision, key, serialized))
957+ else:
958+ self.cursor.execute(
959+ '''
960+ update kv_revisions
961+ set data = ?
962+ where key = ?
963+ and revision = ?''',
964+ [serialized, key, self.revision])
965+
966+ return value
967+
968+ def delta(self, mapping, prefix):
969+ """
970+ return a delta containing values that have changed.
971+ """
972+ previous = self.getrange(prefix, strip=True)
973+ if not previous:
974+ pk = set()
975+ else:
976+ pk = set(previous.keys())
977+ ck = set(mapping.keys())
978+ delta = DeltaSet()
979+
980+ # added
981+ for k in ck.difference(pk):
982+ delta[k] = Delta(None, mapping[k])
983+
984+ # removed
985+ for k in pk.difference(ck):
986+ delta[k] = Delta(previous[k], None)
987+
988+ # changed
989+ for k in pk.intersection(ck):
990+ c = mapping[k]
991+ p = previous[k]
992+ if c != p:
993+ delta[k] = Delta(p, c)
994+
995+ return delta
996+
997+ @contextlib.contextmanager
998+ def hook_scope(self, name=""):
999+ """Scope all future interactions to the current hook execution
1000+ revision."""
1001+ assert not self.revision
1002+ self.cursor.execute(
1003+ 'insert into hooks (hook, date) values (?, ?)',
1004+ (name or sys.argv[0],
1005+ datetime.datetime.utcnow().isoformat()))
1006+ self.revision = self.cursor.lastrowid
1007+ try:
1008+ yield self.revision
1009+ self.revision = None
1010+ except:
1011+ self.flush(False)
1012+ self.revision = None
1013+ raise
1014+ else:
1015+ self.flush()
1016+
1017+ def flush(self, save=True):
1018+ if save:
1019+ self.conn.commit()
1020+ elif self._closed:
1021+ return
1022+ else:
1023+ self.conn.rollback()
1024+
1025+ def _init(self):
1026+ self.cursor.execute('''
1027+ create table if not exists kv (
1028+ key text,
1029+ data text,
1030+ primary key (key)
1031+ )''')
1032+ self.cursor.execute('''
1033+ create table if not exists kv_revisions (
1034+ key text,
1035+ revision integer,
1036+ data text,
1037+ primary key (key, revision)
1038+ )''')
1039+ self.cursor.execute('''
1040+ create table if not exists hooks (
1041+ version integer primary key autoincrement,
1042+ hook text,
1043+ date text
1044+ )''')
1045+ self.conn.commit()
1046+
1047+ def gethistory(self, key, deserialize=False):
1048+ self.cursor.execute(
1049+ '''
1050+ select kv.revision, kv.key, kv.data, h.hook, h.date
1051+ from kv_revisions kv,
1052+ hooks h
1053+ where kv.key=?
1054+ and kv.revision = h.version
1055+ ''', [key])
1056+ if deserialize is False:
1057+ return self.cursor.fetchall()
1058+ return map(_parse_history, self.cursor.fetchall())
1059+
1060+ def debug(self, fh=sys.stderr):
1061+ self.cursor.execute('select * from kv')
1062+ pprint.pprint(self.cursor.fetchall(), stream=fh)
1063+ self.cursor.execute('select * from kv_revisions')
1064+ pprint.pprint(self.cursor.fetchall(), stream=fh)
1065+
1066+
1067+def _parse_history(d):
1068+ return (d[0], d[1], json.loads(d[2]), d[3],
1069+ datetime.datetime.strptime(d[-1], "%Y-%m-%dT%H:%M:%S.%f"))
1070+
1071+
1072+class HookData(object):
1073+ """Simple integration for existing hook exec frameworks.
1074+
1075+ Records all unit information, and stores deltas for processing
1076+ by the hook.
1077+
1078+ Sample::
1079+
1080+ from charmhelper.core import hookenv, unitdata
1081+
1082+ changes = unitdata.HookData()
1083+ db = unitdata.kv()
1084+ hooks = hookenv.Hooks()
1085+
1086+ @hooks.hook
1087+ def config_changed():
1088+ # View all changes to configuration
1089+ for changed, (prev, cur) in changes.conf.items():
1090+ print('config changed', changed,
1091+ 'previous value', prev,
1092+ 'current value', cur)
1093+
1094+ # Get some unit specific bookeeping
1095+ if not db.get('pkg_key'):
1096+ key = urllib.urlopen('https://example.com/pkg_key').read()
1097+ db.set('pkg_key', key)
1098+
1099+ if __name__ == '__main__':
1100+ with changes():
1101+ hook.execute()
1102+
1103+ """
1104+ def __init__(self):
1105+ self.kv = kv()
1106+ self.conf = None
1107+ self.rels = None
1108+
1109+ @contextlib.contextmanager
1110+ def __call__(self):
1111+ from charmhelpers.core import hookenv
1112+ hook_name = hookenv.hook_name()
1113+
1114+ with self.kv.hook_scope(hook_name):
1115+ self._record_charm_version(hookenv.charm_dir())
1116+ delta_config, delta_relation = self._record_hook(hookenv)
1117+ yield self.kv, delta_config, delta_relation
1118+
1119+ def _record_charm_version(self, charm_dir):
1120+ # Record revisions.. charm revisions are meaningless
1121+ # to charm authors as they don't control the revision.
1122+ # so logic dependnent on revision is not particularly
1123+ # useful, however it is useful for debugging analysis.
1124+ charm_rev = open(
1125+ os.path.join(charm_dir, 'revision')).read().strip()
1126+ charm_rev = charm_rev or '0'
1127+ revs = self.kv.get('charm_revisions', [])
1128+ if charm_rev not in revs:
1129+ revs.append(charm_rev.strip() or '0')
1130+ self.kv.set('charm_revisions', revs)
1131+
1132+ def _record_hook(self, hookenv):
1133+ data = hookenv.execution_environment()
1134+ self.conf = conf_delta = self.kv.delta(data['conf'], 'config')
1135+ self.rels = rels_delta = self.kv.delta(data['rels'], 'rels')
1136+ self.kv.set('env', data['env'])
1137+ self.kv.set('unit', data['unit'])
1138+ self.kv.set('relid', data.get('relid'))
1139+ return conf_delta, rels_delta
1140+
1141+
1142+class Record(dict):
1143+
1144+ __slots__ = ()
1145+
1146+ def __getattr__(self, k):
1147+ if k in self:
1148+ return self[k]
1149+ raise AttributeError(k)
1150+
1151+
1152+class DeltaSet(Record):
1153+
1154+ __slots__ = ()
1155+
1156+
1157+Delta = collections.namedtuple('Delta', ['previous', 'current'])
1158+
1159+
1160+_KV = None
1161+
1162+
1163+def kv():
1164+ global _KV
1165+ if _KV is None:
1166+ _KV = Storage()
1167+ return _KV
1168
1169=== removed file 'hooks/charmhelpers/core/unitdata.py'
1170--- hooks/charmhelpers/core/unitdata.py 2015-02-24 11:07:48 +0000
1171+++ hooks/charmhelpers/core/unitdata.py 1970-01-01 00:00:00 +0000
1172@@ -1,477 +0,0 @@
1173-#!/usr/bin/env python
1174-# -*- coding: utf-8 -*-
1175-#
1176-# Copyright 2014-2015 Canonical Limited.
1177-#
1178-# This file is part of charm-helpers.
1179-#
1180-# charm-helpers is free software: you can redistribute it and/or modify
1181-# it under the terms of the GNU Lesser General Public License version 3 as
1182-# published by the Free Software Foundation.
1183-#
1184-# charm-helpers is distributed in the hope that it will be useful,
1185-# but WITHOUT ANY WARRANTY; without even the implied warranty of
1186-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1187-# GNU Lesser General Public License for more details.
1188-#
1189-# You should have received a copy of the GNU Lesser General Public License
1190-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1191-#
1192-#
1193-# Authors:
1194-# Kapil Thangavelu <kapil.foss@gmail.com>
1195-#
1196-"""
1197-Intro
1198------
1199-
1200-A simple way to store state in units. This provides a key value
1201-storage with support for versioned, transactional operation,
1202-and can calculate deltas from previous values to simplify unit logic
1203-when processing changes.
1204-
1205-
1206-Hook Integration
1207-----------------
1208-
1209-There are several extant frameworks for hook execution, including
1210-
1211- - charmhelpers.core.hookenv.Hooks
1212- - charmhelpers.core.services.ServiceManager
1213-
1214-The storage classes are framework agnostic, one simple integration is
1215-via the HookData contextmanager. It will record the current hook
1216-execution environment (including relation data, config data, etc.),
1217-setup a transaction and allow easy access to the changes from
1218-previously seen values. One consequence of the integration is the
1219-reservation of particular keys ('rels', 'unit', 'env', 'config',
1220-'charm_revisions') for their respective values.
1221-
1222-Here's a fully worked integration example using hookenv.Hooks::
1223-
1224- from charmhelper.core import hookenv, unitdata
1225-
1226- hook_data = unitdata.HookData()
1227- db = unitdata.kv()
1228- hooks = hookenv.Hooks()
1229-
1230- @hooks.hook
1231- def config_changed():
1232- # Print all changes to configuration from previously seen
1233- # values.
1234- for changed, (prev, cur) in hook_data.conf.items():
1235- print('config changed', changed,
1236- 'previous value', prev,
1237- 'current value', cur)
1238-
1239- # Get some unit specific bookeeping
1240- if not db.get('pkg_key'):
1241- key = urllib.urlopen('https://example.com/pkg_key').read()
1242- db.set('pkg_key', key)
1243-
1244- # Directly access all charm config as a mapping.
1245- conf = db.getrange('config', True)
1246-
1247- # Directly access all relation data as a mapping
1248- rels = db.getrange('rels', True)
1249-
1250- if __name__ == '__main__':
1251- with hook_data():
1252- hook.execute()
1253-
1254-
1255-A more basic integration is via the hook_scope context manager which simply
1256-manages transaction scope (and records hook name, and timestamp)::
1257-
1258- >>> from unitdata import kv
1259- >>> db = kv()
1260- >>> with db.hook_scope('install'):
1261- ... # do work, in transactional scope.
1262- ... db.set('x', 1)
1263- >>> db.get('x')
1264- 1
1265-
1266-
1267-Usage
1268------
1269-
1270-Values are automatically json de/serialized to preserve basic typing
1271-and complex data struct capabilities (dicts, lists, ints, booleans, etc).
1272-
1273-Individual values can be manipulated via get/set::
1274-
1275- >>> kv.set('y', True)
1276- >>> kv.get('y')
1277- True
1278-
1279- # We can set complex values (dicts, lists) as a single key.
1280- >>> kv.set('config', {'a': 1, 'b': True'})
1281-
1282- # Also supports returning dictionaries as a record which
1283- # provides attribute access.
1284- >>> config = kv.get('config', record=True)
1285- >>> config.b
1286- True
1287-
1288-
1289-Groups of keys can be manipulated with update/getrange::
1290-
1291- >>> kv.update({'z': 1, 'y': 2}, prefix="gui.")
1292- >>> kv.getrange('gui.', strip=True)
1293- {'z': 1, 'y': 2}
1294-
1295-When updating values, its very helpful to understand which values
1296-have actually changed and how have they changed. The storage
1297-provides a delta method to provide for this::
1298-
1299- >>> data = {'debug': True, 'option': 2}
1300- >>> delta = kv.delta(data, 'config.')
1301- >>> delta.debug.previous
1302- None
1303- >>> delta.debug.current
1304- True
1305- >>> delta
1306- {'debug': (None, True), 'option': (None, 2)}
1307-
1308-Note the delta method does not persist the actual change, it needs to
1309-be explicitly saved via 'update' method::
1310-
1311- >>> kv.update(data, 'config.')
1312-
1313-Values modified in the context of a hook scope retain historical values
1314-associated to the hookname.
1315-
1316- >>> with db.hook_scope('config-changed'):
1317- ... db.set('x', 42)
1318- >>> db.gethistory('x')
1319- [(1, u'x', 1, u'install', u'2015-01-21T16:49:30.038372'),
1320- (2, u'x', 42, u'config-changed', u'2015-01-21T16:49:30.038786')]
1321-
1322-"""
1323-
1324-import collections
1325-import contextlib
1326-import datetime
1327-import json
1328-import os
1329-import pprint
1330-import sqlite3
1331-import sys
1332-
1333-__author__ = 'Kapil Thangavelu <kapil.foss@gmail.com>'
1334-
1335-
1336-class Storage(object):
1337- """Simple key value database for local unit state within charms.
1338-
1339- Modifications are automatically committed at hook exit. That's
1340- currently regardless of exit code.
1341-
1342- To support dicts, lists, integer, floats, and booleans values
1343- are automatically json encoded/decoded.
1344- """
1345- def __init__(self, path=None):
1346- self.db_path = path
1347- if path is None:
1348- self.db_path = os.path.join(
1349- os.environ.get('CHARM_DIR', ''), '.unit-state.db')
1350- self.conn = sqlite3.connect('%s' % self.db_path)
1351- self.cursor = self.conn.cursor()
1352- self.revision = None
1353- self._closed = False
1354- self._init()
1355-
1356- def close(self):
1357- if self._closed:
1358- return
1359- self.flush(False)
1360- self.cursor.close()
1361- self.conn.close()
1362- self._closed = True
1363-
1364- def _scoped_query(self, stmt, params=None):
1365- if params is None:
1366- params = []
1367- return stmt, params
1368-
1369- def get(self, key, default=None, record=False):
1370- self.cursor.execute(
1371- *self._scoped_query(
1372- 'select data from kv where key=?', [key]))
1373- result = self.cursor.fetchone()
1374- if not result:
1375- return default
1376- if record:
1377- return Record(json.loads(result[0]))
1378- return json.loads(result[0])
1379-
1380- def getrange(self, key_prefix, strip=False):
1381- stmt = "select key, data from kv where key like '%s%%'" % key_prefix
1382- self.cursor.execute(*self._scoped_query(stmt))
1383- result = self.cursor.fetchall()
1384-
1385- if not result:
1386- return None
1387- if not strip:
1388- key_prefix = ''
1389- return dict([
1390- (k[len(key_prefix):], json.loads(v)) for k, v in result])
1391-
1392- def update(self, mapping, prefix=""):
1393- for k, v in mapping.items():
1394- self.set("%s%s" % (prefix, k), v)
1395-
1396- def unset(self, key):
1397- self.cursor.execute('delete from kv where key=?', [key])
1398- if self.revision and self.cursor.rowcount:
1399- self.cursor.execute(
1400- 'insert into kv_revisions values (?, ?, ?)',
1401- [key, self.revision, json.dumps('DELETED')])
1402-
1403- def set(self, key, value):
1404- serialized = json.dumps(value)
1405-
1406- self.cursor.execute(
1407- 'select data from kv where key=?', [key])
1408- exists = self.cursor.fetchone()
1409-
1410- # Skip mutations to the same value
1411- if exists:
1412- if exists[0] == serialized:
1413- return value
1414-
1415- if not exists:
1416- self.cursor.execute(
1417- 'insert into kv (key, data) values (?, ?)',
1418- (key, serialized))
1419- else:
1420- self.cursor.execute('''
1421- update kv
1422- set data = ?
1423- where key = ?''', [serialized, key])
1424-
1425- # Save
1426- if not self.revision:
1427- return value
1428-
1429- self.cursor.execute(
1430- 'select 1 from kv_revisions where key=? and revision=?',
1431- [key, self.revision])
1432- exists = self.cursor.fetchone()
1433-
1434- if not exists:
1435- self.cursor.execute(
1436- '''insert into kv_revisions (
1437- revision, key, data) values (?, ?, ?)''',
1438- (self.revision, key, serialized))
1439- else:
1440- self.cursor.execute(
1441- '''
1442- update kv_revisions
1443- set data = ?
1444- where key = ?
1445- and revision = ?''',
1446- [serialized, key, self.revision])
1447-
1448- return value
1449-
1450- def delta(self, mapping, prefix):
1451- """
1452- return a delta containing values that have changed.
1453- """
1454- previous = self.getrange(prefix, strip=True)
1455- if not previous:
1456- pk = set()
1457- else:
1458- pk = set(previous.keys())
1459- ck = set(mapping.keys())
1460- delta = DeltaSet()
1461-
1462- # added
1463- for k in ck.difference(pk):
1464- delta[k] = Delta(None, mapping[k])
1465-
1466- # removed
1467- for k in pk.difference(ck):
1468- delta[k] = Delta(previous[k], None)
1469-
1470- # changed
1471- for k in pk.intersection(ck):
1472- c = mapping[k]
1473- p = previous[k]
1474- if c != p:
1475- delta[k] = Delta(p, c)
1476-
1477- return delta
1478-
1479- @contextlib.contextmanager
1480- def hook_scope(self, name=""):
1481- """Scope all future interactions to the current hook execution
1482- revision."""
1483- assert not self.revision
1484- self.cursor.execute(
1485- 'insert into hooks (hook, date) values (?, ?)',
1486- (name or sys.argv[0],
1487- datetime.datetime.utcnow().isoformat()))
1488- self.revision = self.cursor.lastrowid
1489- try:
1490- yield self.revision
1491- self.revision = None
1492- except:
1493- self.flush(False)
1494- self.revision = None
1495- raise
1496- else:
1497- self.flush()
1498-
1499- def flush(self, save=True):
1500- if save:
1501- self.conn.commit()
1502- elif self._closed:
1503- return
1504- else:
1505- self.conn.rollback()
1506-
1507- def _init(self):
1508- self.cursor.execute('''
1509- create table if not exists kv (
1510- key text,
1511- data text,
1512- primary key (key)
1513- )''')
1514- self.cursor.execute('''
1515- create table if not exists kv_revisions (
1516- key text,
1517- revision integer,
1518- data text,
1519- primary key (key, revision)
1520- )''')
1521- self.cursor.execute('''
1522- create table if not exists hooks (
1523- version integer primary key autoincrement,
1524- hook text,
1525- date text
1526- )''')
1527- self.conn.commit()
1528-
1529- def gethistory(self, key, deserialize=False):
1530- self.cursor.execute(
1531- '''
1532- select kv.revision, kv.key, kv.data, h.hook, h.date
1533- from kv_revisions kv,
1534- hooks h
1535- where kv.key=?
1536- and kv.revision = h.version
1537- ''', [key])
1538- if deserialize is False:
1539- return self.cursor.fetchall()
1540- return map(_parse_history, self.cursor.fetchall())
1541-
1542- def debug(self, fh=sys.stderr):
1543- self.cursor.execute('select * from kv')
1544- pprint.pprint(self.cursor.fetchall(), stream=fh)
1545- self.cursor.execute('select * from kv_revisions')
1546- pprint.pprint(self.cursor.fetchall(), stream=fh)
1547-
1548-
1549-def _parse_history(d):
1550- return (d[0], d[1], json.loads(d[2]), d[3],
1551- datetime.datetime.strptime(d[-1], "%Y-%m-%dT%H:%M:%S.%f"))
1552-
1553-
1554-class HookData(object):
1555- """Simple integration for existing hook exec frameworks.
1556-
1557- Records all unit information, and stores deltas for processing
1558- by the hook.
1559-
1560- Sample::
1561-
1562- from charmhelper.core import hookenv, unitdata
1563-
1564- changes = unitdata.HookData()
1565- db = unitdata.kv()
1566- hooks = hookenv.Hooks()
1567-
1568- @hooks.hook
1569- def config_changed():
1570- # View all changes to configuration
1571- for changed, (prev, cur) in changes.conf.items():
1572- print('config changed', changed,
1573- 'previous value', prev,
1574- 'current value', cur)
1575-
1576- # Get some unit specific bookeeping
1577- if not db.get('pkg_key'):
1578- key = urllib.urlopen('https://example.com/pkg_key').read()
1579- db.set('pkg_key', key)
1580-
1581- if __name__ == '__main__':
1582- with changes():
1583- hook.execute()
1584-
1585- """
1586- def __init__(self):
1587- self.kv = kv()
1588- self.conf = None
1589- self.rels = None
1590-
1591- @contextlib.contextmanager
1592- def __call__(self):
1593- from charmhelpers.core import hookenv
1594- hook_name = hookenv.hook_name()
1595-
1596- with self.kv.hook_scope(hook_name):
1597- self._record_charm_version(hookenv.charm_dir())
1598- delta_config, delta_relation = self._record_hook(hookenv)
1599- yield self.kv, delta_config, delta_relation
1600-
1601- def _record_charm_version(self, charm_dir):
1602- # Record revisions.. charm revisions are meaningless
1603- # to charm authors as they don't control the revision.
1604- # so logic dependnent on revision is not particularly
1605- # useful, however it is useful for debugging analysis.
1606- charm_rev = open(
1607- os.path.join(charm_dir, 'revision')).read().strip()
1608- charm_rev = charm_rev or '0'
1609- revs = self.kv.get('charm_revisions', [])
1610- if charm_rev not in revs:
1611- revs.append(charm_rev.strip() or '0')
1612- self.kv.set('charm_revisions', revs)
1613-
1614- def _record_hook(self, hookenv):
1615- data = hookenv.execution_environment()
1616- self.conf = conf_delta = self.kv.delta(data['conf'], 'config')
1617- self.rels = rels_delta = self.kv.delta(data['rels'], 'rels')
1618- self.kv.set('env', data['env'])
1619- self.kv.set('unit', data['unit'])
1620- self.kv.set('relid', data.get('relid'))
1621- return conf_delta, rels_delta
1622-
1623-
1624-class Record(dict):
1625-
1626- __slots__ = ()
1627-
1628- def __getattr__(self, k):
1629- if k in self:
1630- return self[k]
1631- raise AttributeError(k)
1632-
1633-
1634-class DeltaSet(Record):
1635-
1636- __slots__ = ()
1637-
1638-
1639-Delta = collections.namedtuple('Delta', ['previous', 'current'])
1640-
1641-
1642-_KV = None
1643-
1644-
1645-def kv():
1646- global _KV
1647- if _KV is None:
1648- _KV = Storage()
1649- return _KV

Subscribers

People subscribed via source and target branches