Merge lp:~brad-marshall/charms/trusty/ceilometer/add-haproxy-nrpe-fix-servicegroups into lp:~openstack-charmers-archive/charms/trusty/ceilometer/next

Proposed by Brad Marshall
Status: Merged
Merged at revision: 70
Proposed branch: lp:~brad-marshall/charms/trusty/ceilometer/add-haproxy-nrpe-fix-servicegroups
Merge into: lp:~openstack-charmers-archive/charms/trusty/ceilometer/next
Diff against target: 1082 lines (+734/-42)
19 files modified
config.yaml (+6/-0)
hooks/ceilometer_hooks.py (+2/-0)
hooks/charmhelpers/contrib/charmsupport/nrpe.py (+41/-7)
hooks/charmhelpers/contrib/hahelpers/cluster.py (+5/-1)
hooks/charmhelpers/contrib/openstack/amulet/deployment.py (+5/-2)
hooks/charmhelpers/contrib/openstack/files/__init__.py (+18/-0)
hooks/charmhelpers/contrib/openstack/files/check_haproxy.sh (+32/-0)
hooks/charmhelpers/contrib/openstack/files/check_haproxy_queue_depth.sh (+30/-0)
hooks/charmhelpers/contrib/openstack/ip.py (+37/-0)
hooks/charmhelpers/contrib/openstack/utils.py (+1/-0)
hooks/charmhelpers/contrib/python/packages.py (+2/-2)
hooks/charmhelpers/core/fstab.py (+4/-4)
hooks/charmhelpers/core/host.py (+5/-5)
hooks/charmhelpers/core/strutils.py (+42/-0)
hooks/charmhelpers/core/sysctl.py (+13/-7)
hooks/charmhelpers/core/templating.py (+3/-3)
hooks/charmhelpers/core/unitdata.py (+477/-0)
hooks/charmhelpers/fetch/archiveurl.py (+10/-10)
hooks/charmhelpers/fetch/giturl.py (+1/-1)
To merge this branch: bzr merge lp:~brad-marshall/charms/trusty/ceilometer/add-haproxy-nrpe-fix-servicegroups
Reviewer Review Type Date Requested Status
Liam Young (community) Approve
Review via email: mp+250713@code.launchpad.net

Description of the change

Synced charmhelpers, added nagios_servicegroup config option, and added haproxy nrpe checks.

To post a comment you must log in.
Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_lint_check #2228 ceilometer-next for brad-marshall mp250713
    LINT OK: passed

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

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

charm_unit_test #2017 ceilometer-next for brad-marshall mp250713
    UNIT OK: passed

Build: http://10.245.162.77:8080/job/charm_unit_test/2017/

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

charm_amulet_test #2174 ceilometer-next for brad-marshall mp250713
    AMULET FAIL: amulet-test missing

AMULET Results (max last 2 lines):
INFO:root:Search string not found in makefile target commands.
ERROR:root:No make target was executed.

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

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-01-09 16:27:03 +0000
3+++ config.yaml 2015-02-24 06:29:10 +0000
4@@ -68,6 +68,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 # Network configuration options
15 # by default all access is over 'private-address'
16 os-admin-network:
17
18=== modified file 'hooks/ceilometer_hooks.py'
19--- hooks/ceilometer_hooks.py 2015-01-12 12:03:58 +0000
20+++ hooks/ceilometer_hooks.py 2015-02-24 06:29:10 +0000
21@@ -289,7 +289,9 @@
22 hostname = nrpe.get_nagios_hostname()
23 current_unit = nrpe.get_nagios_unit_name()
24 nrpe_setup = nrpe.NRPE(hostname=hostname)
25+ nrpe.copy_nrpe_checks()
26 nrpe.add_init_service_checks(nrpe_setup, services(), current_unit)
27+ nrpe.add_haproxy_checks(nrpe_setup, current_unit)
28 nrpe_setup.write()
29
30
31
32=== modified file 'hooks/charmhelpers/contrib/charmsupport/nrpe.py'
33--- hooks/charmhelpers/contrib/charmsupport/nrpe.py 2015-01-26 09:48:14 +0000
34+++ hooks/charmhelpers/contrib/charmsupport/nrpe.py 2015-02-24 06:29:10 +0000
35@@ -24,6 +24,8 @@
36 import pwd
37 import grp
38 import os
39+import glob
40+import shutil
41 import re
42 import shlex
43 import yaml
44@@ -161,7 +163,7 @@
45 log('Check command not found: {}'.format(parts[0]))
46 return ''
47
48- def write(self, nagios_context, hostname, nagios_servicegroups=None):
49+ def write(self, nagios_context, hostname, nagios_servicegroups):
50 nrpe_check_file = '/etc/nagios/nrpe.d/{}.cfg'.format(
51 self.command)
52 with open(nrpe_check_file, 'w') as nrpe_check_config:
53@@ -177,14 +179,11 @@
54 nagios_servicegroups)
55
56 def write_service_config(self, nagios_context, hostname,
57- nagios_servicegroups=None):
58+ nagios_servicegroups):
59 for f in os.listdir(NRPE.nagios_exportdir):
60 if re.search('.*{}.cfg'.format(self.command), f):
61 os.remove(os.path.join(NRPE.nagios_exportdir, f))
62
63- if not nagios_servicegroups:
64- nagios_servicegroups = nagios_context
65-
66 templ_vars = {
67 'nagios_hostname': hostname,
68 'nagios_servicegroup': nagios_servicegroups,
69@@ -211,10 +210,10 @@
70 super(NRPE, self).__init__()
71 self.config = config()
72 self.nagios_context = self.config['nagios_context']
73- if 'nagios_servicegroups' in self.config:
74+ if 'nagios_servicegroups' in self.config and self.config['nagios_servicegroups']:
75 self.nagios_servicegroups = self.config['nagios_servicegroups']
76 else:
77- self.nagios_servicegroups = 'juju'
78+ self.nagios_servicegroups = self.nagios_context
79 self.unit_name = local_unit().replace('/', '-')
80 if hostname:
81 self.hostname = hostname
82@@ -322,3 +321,38 @@
83 check_cmd='check_status_file.py -f '
84 '/var/lib/nagios/service-check-%s.txt' % svc,
85 )
86+
87+
88+def copy_nrpe_checks():
89+ """
90+ Copy the nrpe checks into place
91+
92+ """
93+ NAGIOS_PLUGINS = '/usr/local/lib/nagios/plugins'
94+ nrpe_files_dir = os.path.join(os.getenv('CHARM_DIR'), 'hooks',
95+ 'charmhelpers', 'contrib', 'openstack',
96+ 'files')
97+
98+ if not os.path.exists(NAGIOS_PLUGINS):
99+ os.makedirs(NAGIOS_PLUGINS)
100+ for fname in glob.glob(os.path.join(nrpe_files_dir, "check_*")):
101+ if os.path.isfile(fname):
102+ shutil.copy2(fname,
103+ os.path.join(NAGIOS_PLUGINS, os.path.basename(fname)))
104+
105+
106+def add_haproxy_checks(nrpe, unit_name):
107+ """
108+ Add checks for each service in list
109+
110+ :param NRPE nrpe: NRPE object to add check to
111+ :param str unit_name: Unit name to use in check description
112+ """
113+ nrpe.add_check(
114+ shortname='haproxy_servers',
115+ description='Check HAProxy {%s}' % unit_name,
116+ check_cmd='check_haproxy.sh')
117+ nrpe.add_check(
118+ shortname='haproxy_queue',
119+ description='Check HAProxy queue depth {%s}' % unit_name,
120+ check_cmd='check_haproxy_queue_depth.sh')
121
122=== modified file 'hooks/charmhelpers/contrib/hahelpers/cluster.py'
123--- hooks/charmhelpers/contrib/hahelpers/cluster.py 2015-01-26 09:48:14 +0000
124+++ hooks/charmhelpers/contrib/hahelpers/cluster.py 2015-02-24 06:29:10 +0000
125@@ -48,6 +48,9 @@
126 from charmhelpers.core.decorators import (
127 retry_on_exception,
128 )
129+from charmhelpers.core.strutils import (
130+ bool_from_string,
131+)
132
133
134 class HAIncompleteConfig(Exception):
135@@ -164,7 +167,8 @@
136 .
137 returns: boolean
138 '''
139- if config_get('use-https') == "yes":
140+ use_https = config_get('use-https')
141+ if use_https and bool_from_string(use_https):
142 return True
143 if config_get('ssl_cert') and config_get('ssl_key'):
144 return True
145
146=== modified file 'hooks/charmhelpers/contrib/openstack/amulet/deployment.py'
147--- hooks/charmhelpers/contrib/openstack/amulet/deployment.py 2015-01-26 09:48:14 +0000
148+++ hooks/charmhelpers/contrib/openstack/amulet/deployment.py 2015-02-24 06:29:10 +0000
149@@ -71,16 +71,19 @@
150 services.append(this_service)
151 use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph',
152 'ceph-osd', 'ceph-radosgw']
153+ # Openstack subordinate charms do not expose an origin option as that
154+ # is controlled by the principle
155+ ignore = ['neutron-openvswitch']
156
157 if self.openstack:
158 for svc in services:
159- if svc['name'] not in use_source:
160+ if svc['name'] not in use_source + ignore:
161 config = {'openstack-origin': self.openstack}
162 self.d.configure(svc['name'], config)
163
164 if self.source:
165 for svc in services:
166- if svc['name'] in use_source:
167+ if svc['name'] in use_source and svc['name'] not in ignore:
168 config = {'source': self.source}
169 self.d.configure(svc['name'], config)
170
171
172=== added directory 'hooks/charmhelpers/contrib/openstack/files'
173=== added file 'hooks/charmhelpers/contrib/openstack/files/__init__.py'
174--- hooks/charmhelpers/contrib/openstack/files/__init__.py 1970-01-01 00:00:00 +0000
175+++ hooks/charmhelpers/contrib/openstack/files/__init__.py 2015-02-24 06:29:10 +0000
176@@ -0,0 +1,18 @@
177+# Copyright 2014-2015 Canonical Limited.
178+#
179+# This file is part of charm-helpers.
180+#
181+# charm-helpers is free software: you can redistribute it and/or modify
182+# it under the terms of the GNU Lesser General Public License version 3 as
183+# published by the Free Software Foundation.
184+#
185+# charm-helpers is distributed in the hope that it will be useful,
186+# but WITHOUT ANY WARRANTY; without even the implied warranty of
187+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
188+# GNU Lesser General Public License for more details.
189+#
190+# You should have received a copy of the GNU Lesser General Public License
191+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
192+
193+# dummy __init__.py to fool syncer into thinking this is a syncable python
194+# module
195
196=== added file 'hooks/charmhelpers/contrib/openstack/files/check_haproxy.sh'
197--- hooks/charmhelpers/contrib/openstack/files/check_haproxy.sh 1970-01-01 00:00:00 +0000
198+++ hooks/charmhelpers/contrib/openstack/files/check_haproxy.sh 2015-02-24 06:29:10 +0000
199@@ -0,0 +1,32 @@
200+#!/bin/bash
201+#--------------------------------------------
202+# This file is managed by Juju
203+#--------------------------------------------
204+#
205+# Copyright 2009,2012 Canonical Ltd.
206+# Author: Tom Haddon
207+
208+CRITICAL=0
209+NOTACTIVE=''
210+LOGFILE=/var/log/nagios/check_haproxy.log
211+AUTH=$(grep -r "stats auth" /etc/haproxy | head -1 | awk '{print $4}')
212+
213+for appserver in $(grep ' server' /etc/haproxy/haproxy.cfg | awk '{print $2'});
214+do
215+ 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')
216+ if [ $? != 0 ]; then
217+ date >> $LOGFILE
218+ echo $output >> $LOGFILE
219+ /usr/lib/nagios/plugins/check_http -a ${AUTH} -I 127.0.0.1 -p 8888 -v | grep $appserver >> $LOGFILE 2>&1
220+ CRITICAL=1
221+ NOTACTIVE="${NOTACTIVE} $appserver"
222+ fi
223+done
224+
225+if [ $CRITICAL = 1 ]; then
226+ echo "CRITICAL:${NOTACTIVE}"
227+ exit 2
228+fi
229+
230+echo "OK: All haproxy instances looking good"
231+exit 0
232
233=== added file 'hooks/charmhelpers/contrib/openstack/files/check_haproxy_queue_depth.sh'
234--- hooks/charmhelpers/contrib/openstack/files/check_haproxy_queue_depth.sh 1970-01-01 00:00:00 +0000
235+++ hooks/charmhelpers/contrib/openstack/files/check_haproxy_queue_depth.sh 2015-02-24 06:29:10 +0000
236@@ -0,0 +1,30 @@
237+#!/bin/bash
238+#--------------------------------------------
239+# This file is managed by Juju
240+#--------------------------------------------
241+#
242+# Copyright 2009,2012 Canonical Ltd.
243+# Author: Tom Haddon
244+
245+# These should be config options at some stage
246+CURRQthrsh=0
247+MAXQthrsh=100
248+
249+AUTH=$(grep -r "stats auth" /etc/haproxy | head -1 | awk '{print $4}')
250+
251+HAPROXYSTATS=$(/usr/lib/nagios/plugins/check_http -a ${AUTH} -I 127.0.0.1 -p 8888 -u '/;csv' -v)
252+
253+for BACKEND in $(echo $HAPROXYSTATS| xargs -n1 | grep BACKEND | awk -F , '{print $1}')
254+do
255+ CURRQ=$(echo "$HAPROXYSTATS" | grep $BACKEND | grep BACKEND | cut -d , -f 3)
256+ MAXQ=$(echo "$HAPROXYSTATS" | grep $BACKEND | grep BACKEND | cut -d , -f 4)
257+
258+ if [[ $CURRQ -gt $CURRQthrsh || $MAXQ -gt $MAXQthrsh ]] ; then
259+ echo "CRITICAL: queue depth for $BACKEND - CURRENT:$CURRQ MAX:$MAXQ"
260+ exit 2
261+ fi
262+done
263+
264+echo "OK: All haproxy queue depths looking good"
265+exit 0
266+
267
268=== modified file 'hooks/charmhelpers/contrib/openstack/ip.py'
269--- hooks/charmhelpers/contrib/openstack/ip.py 2015-01-26 09:48:14 +0000
270+++ hooks/charmhelpers/contrib/openstack/ip.py 2015-02-24 06:29:10 +0000
271@@ -26,6 +26,8 @@
272 )
273 from charmhelpers.contrib.hahelpers.cluster import is_clustered
274
275+from functools import partial
276+
277 PUBLIC = 'public'
278 INTERNAL = 'int'
279 ADMIN = 'admin'
280@@ -107,3 +109,38 @@
281 "clustered=%s)" % (net_type, clustered))
282
283 return resolved_address
284+
285+
286+def endpoint_url(configs, url_template, port, endpoint_type=PUBLIC,
287+ override=None):
288+ """Returns the correct endpoint URL to advertise to Keystone.
289+
290+ This method provides the correct endpoint URL which should be advertised to
291+ the keystone charm for endpoint creation. This method allows for the url to
292+ be overridden to force a keystone endpoint to have specific URL for any of
293+ the defined scopes (admin, internal, public).
294+
295+ :param configs: OSTemplateRenderer config templating object to inspect
296+ for a complete https context.
297+ :param url_template: str format string for creating the url template. Only
298+ two values will be passed - the scheme+hostname
299+ returned by the canonical_url and the port.
300+ :param endpoint_type: str endpoint type to resolve.
301+ :param override: str the name of the config option which overrides the
302+ endpoint URL defined by the charm itself. None will
303+ disable any overrides (default).
304+ """
305+ if override:
306+ # Return any user-defined overrides for the keystone endpoint URL.
307+ user_value = config(override)
308+ if user_value:
309+ return user_value.strip()
310+
311+ return url_template % (canonical_url(configs, endpoint_type), port)
312+
313+
314+public_endpoint = partial(endpoint_url, endpoint_type=PUBLIC)
315+
316+internal_endpoint = partial(endpoint_url, endpoint_type=INTERNAL)
317+
318+admin_endpoint = partial(endpoint_url, endpoint_type=ADMIN)
319
320=== modified file 'hooks/charmhelpers/contrib/openstack/utils.py'
321--- hooks/charmhelpers/contrib/openstack/utils.py 2015-01-26 09:48:14 +0000
322+++ hooks/charmhelpers/contrib/openstack/utils.py 2015-02-24 06:29:10 +0000
323@@ -103,6 +103,7 @@
324 ('2.1.0', 'juno'),
325 ('2.2.0', 'juno'),
326 ('2.2.1', 'kilo'),
327+ ('2.2.2', 'kilo'),
328 ])
329
330 DEFAULT_LOOPBACK_SIZE = '5G'
331
332=== modified file 'hooks/charmhelpers/contrib/python/packages.py'
333--- hooks/charmhelpers/contrib/python/packages.py 2015-01-26 09:48:14 +0000
334+++ hooks/charmhelpers/contrib/python/packages.py 2015-02-24 06:29:10 +0000
335@@ -17,8 +17,6 @@
336 # You should have received a copy of the GNU Lesser General Public License
337 # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
338
339-__author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>"
340-
341 from charmhelpers.fetch import apt_install, apt_update
342 from charmhelpers.core.hookenv import log
343
344@@ -29,6 +27,8 @@
345 apt_install('python-pip')
346 from pip import main as pip_execute
347
348+__author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>"
349+
350
351 def parse_options(given, available):
352 """Given a set of options, check if available"""
353
354=== modified file 'hooks/charmhelpers/core/fstab.py'
355--- hooks/charmhelpers/core/fstab.py 2015-01-26 09:48:14 +0000
356+++ hooks/charmhelpers/core/fstab.py 2015-02-24 06:29:10 +0000
357@@ -17,11 +17,11 @@
358 # You should have received a copy of the GNU Lesser General Public License
359 # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
360
361-__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
362-
363 import io
364 import os
365
366+__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
367+
368
369 class Fstab(io.FileIO):
370 """This class extends file in order to implement a file reader/writer
371@@ -77,7 +77,7 @@
372 for line in self.readlines():
373 line = line.decode('us-ascii')
374 try:
375- if line.strip() and not line.startswith("#"):
376+ if line.strip() and not line.strip().startswith("#"):
377 yield self._hydrate_entry(line)
378 except ValueError:
379 pass
380@@ -104,7 +104,7 @@
381
382 found = False
383 for index, line in enumerate(lines):
384- if not line.startswith("#"):
385+ if line.strip() and not line.strip().startswith("#"):
386 if self._hydrate_entry(line) == entry:
387 found = True
388 break
389
390=== modified file 'hooks/charmhelpers/core/host.py'
391--- hooks/charmhelpers/core/host.py 2015-01-26 09:48:14 +0000
392+++ hooks/charmhelpers/core/host.py 2015-02-24 06:29:10 +0000
393@@ -191,11 +191,11 @@
394
395
396 def write_file(path, content, owner='root', group='root', perms=0o444):
397- """Create or overwrite a file with the contents of a string"""
398+ """Create or overwrite a file with the contents of a byte string."""
399 log("Writing file {} {}:{} {:o}".format(path, owner, group, perms))
400 uid = pwd.getpwnam(owner).pw_uid
401 gid = grp.getgrnam(group).gr_gid
402- with open(path, 'w') as target:
403+ with open(path, 'wb') as target:
404 os.fchown(target.fileno(), uid, gid)
405 os.fchmod(target.fileno(), perms)
406 target.write(content)
407@@ -305,11 +305,11 @@
408 ceph_client_changed function.
409 """
410 def wrap(f):
411- def wrapped_f(*args):
412+ def wrapped_f(*args, **kwargs):
413 checksums = {}
414 for path in restart_map:
415 checksums[path] = file_hash(path)
416- f(*args)
417+ f(*args, **kwargs)
418 restarts = []
419 for path in restart_map:
420 if checksums[path] != file_hash(path):
421@@ -361,7 +361,7 @@
422 ip_output = (line for line in ip_output if line)
423 for line in ip_output:
424 if line.split()[1].startswith(int_type):
425- matched = re.search('.*: (bond[0-9]+\.[0-9]+)@.*', line)
426+ matched = re.search('.*: (' + int_type + r'[0-9]+\.[0-9]+)@.*', line)
427 if matched:
428 interface = matched.groups()[0]
429 else:
430
431=== added file 'hooks/charmhelpers/core/strutils.py'
432--- hooks/charmhelpers/core/strutils.py 1970-01-01 00:00:00 +0000
433+++ hooks/charmhelpers/core/strutils.py 2015-02-24 06:29:10 +0000
434@@ -0,0 +1,42 @@
435+#!/usr/bin/env python
436+# -*- coding: utf-8 -*-
437+
438+# Copyright 2014-2015 Canonical Limited.
439+#
440+# This file is part of charm-helpers.
441+#
442+# charm-helpers is free software: you can redistribute it and/or modify
443+# it under the terms of the GNU Lesser General Public License version 3 as
444+# published by the Free Software Foundation.
445+#
446+# charm-helpers is distributed in the hope that it will be useful,
447+# but WITHOUT ANY WARRANTY; without even the implied warranty of
448+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
449+# GNU Lesser General Public License for more details.
450+#
451+# You should have received a copy of the GNU Lesser General Public License
452+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
453+
454+import six
455+
456+
457+def bool_from_string(value):
458+ """Interpret string value as boolean.
459+
460+ Returns True if value translates to True otherwise False.
461+ """
462+ if isinstance(value, six.string_types):
463+ value = six.text_type(value)
464+ else:
465+ msg = "Unable to interpret non-string value '%s' as boolean" % (value)
466+ raise ValueError(msg)
467+
468+ value = value.strip().lower()
469+
470+ if value in ['y', 'yes', 'true', 't']:
471+ return True
472+ elif value in ['n', 'no', 'false', 'f']:
473+ return False
474+
475+ msg = "Unable to interpret string value '%s' as boolean" % (value)
476+ raise ValueError(msg)
477
478=== modified file 'hooks/charmhelpers/core/sysctl.py'
479--- hooks/charmhelpers/core/sysctl.py 2015-01-26 09:48:14 +0000
480+++ hooks/charmhelpers/core/sysctl.py 2015-02-24 06:29:10 +0000
481@@ -17,8 +17,6 @@
482 # You should have received a copy of the GNU Lesser General Public License
483 # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
484
485-__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
486-
487 import yaml
488
489 from subprocess import check_call
490@@ -26,25 +24,33 @@
491 from charmhelpers.core.hookenv import (
492 log,
493 DEBUG,
494+ ERROR,
495 )
496
497+__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
498+
499
500 def create(sysctl_dict, sysctl_file):
501 """Creates a sysctl.conf file from a YAML associative array
502
503- :param sysctl_dict: a dict of sysctl options eg { 'kernel.max_pid': 1337 }
504- :type sysctl_dict: dict
505+ :param sysctl_dict: a YAML-formatted string of sysctl options eg "{ 'kernel.max_pid': 1337 }"
506+ :type sysctl_dict: str
507 :param sysctl_file: path to the sysctl file to be saved
508 :type sysctl_file: str or unicode
509 :returns: None
510 """
511- sysctl_dict = yaml.load(sysctl_dict)
512+ try:
513+ sysctl_dict_parsed = yaml.safe_load(sysctl_dict)
514+ except yaml.YAMLError:
515+ log("Error parsing YAML sysctl_dict: {}".format(sysctl_dict),
516+ level=ERROR)
517+ return
518
519 with open(sysctl_file, "w") as fd:
520- for key, value in sysctl_dict.items():
521+ for key, value in sysctl_dict_parsed.items():
522 fd.write("{}={}\n".format(key, value))
523
524- log("Updating sysctl_file: %s values: %s" % (sysctl_file, sysctl_dict),
525+ log("Updating sysctl_file: %s values: %s" % (sysctl_file, sysctl_dict_parsed),
526 level=DEBUG)
527
528 check_call(["sysctl", "-p", sysctl_file])
529
530=== modified file 'hooks/charmhelpers/core/templating.py'
531--- hooks/charmhelpers/core/templating.py 2015-01-26 09:48:14 +0000
532+++ hooks/charmhelpers/core/templating.py 2015-02-24 06:29:10 +0000
533@@ -21,7 +21,7 @@
534
535
536 def render(source, target, context, owner='root', group='root',
537- perms=0o444, templates_dir=None):
538+ perms=0o444, templates_dir=None, encoding='UTF-8'):
539 """
540 Render a template.
541
542@@ -64,5 +64,5 @@
543 level=hookenv.ERROR)
544 raise e
545 content = template.render(context)
546- host.mkdir(os.path.dirname(target), owner, group)
547- host.write_file(target, content, owner, group, perms)
548+ host.mkdir(os.path.dirname(target), owner, group, perms=0o755)
549+ host.write_file(target, content.encode(encoding), owner, group, perms)
550
551=== added file 'hooks/charmhelpers/core/unitdata.py'
552--- hooks/charmhelpers/core/unitdata.py 1970-01-01 00:00:00 +0000
553+++ hooks/charmhelpers/core/unitdata.py 2015-02-24 06:29:10 +0000
554@@ -0,0 +1,477 @@
555+#!/usr/bin/env python
556+# -*- coding: utf-8 -*-
557+#
558+# Copyright 2014-2015 Canonical Limited.
559+#
560+# This file is part of charm-helpers.
561+#
562+# charm-helpers is free software: you can redistribute it and/or modify
563+# it under the terms of the GNU Lesser General Public License version 3 as
564+# published by the Free Software Foundation.
565+#
566+# charm-helpers is distributed in the hope that it will be useful,
567+# but WITHOUT ANY WARRANTY; without even the implied warranty of
568+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
569+# GNU Lesser General Public License for more details.
570+#
571+# You should have received a copy of the GNU Lesser General Public License
572+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
573+#
574+#
575+# Authors:
576+# Kapil Thangavelu <kapil.foss@gmail.com>
577+#
578+"""
579+Intro
580+-----
581+
582+A simple way to store state in units. This provides a key value
583+storage with support for versioned, transactional operation,
584+and can calculate deltas from previous values to simplify unit logic
585+when processing changes.
586+
587+
588+Hook Integration
589+----------------
590+
591+There are several extant frameworks for hook execution, including
592+
593+ - charmhelpers.core.hookenv.Hooks
594+ - charmhelpers.core.services.ServiceManager
595+
596+The storage classes are framework agnostic, one simple integration is
597+via the HookData contextmanager. It will record the current hook
598+execution environment (including relation data, config data, etc.),
599+setup a transaction and allow easy access to the changes from
600+previously seen values. One consequence of the integration is the
601+reservation of particular keys ('rels', 'unit', 'env', 'config',
602+'charm_revisions') for their respective values.
603+
604+Here's a fully worked integration example using hookenv.Hooks::
605+
606+ from charmhelper.core import hookenv, unitdata
607+
608+ hook_data = unitdata.HookData()
609+ db = unitdata.kv()
610+ hooks = hookenv.Hooks()
611+
612+ @hooks.hook
613+ def config_changed():
614+ # Print all changes to configuration from previously seen
615+ # values.
616+ for changed, (prev, cur) in hook_data.conf.items():
617+ print('config changed', changed,
618+ 'previous value', prev,
619+ 'current value', cur)
620+
621+ # Get some unit specific bookeeping
622+ if not db.get('pkg_key'):
623+ key = urllib.urlopen('https://example.com/pkg_key').read()
624+ db.set('pkg_key', key)
625+
626+ # Directly access all charm config as a mapping.
627+ conf = db.getrange('config', True)
628+
629+ # Directly access all relation data as a mapping
630+ rels = db.getrange('rels', True)
631+
632+ if __name__ == '__main__':
633+ with hook_data():
634+ hook.execute()
635+
636+
637+A more basic integration is via the hook_scope context manager which simply
638+manages transaction scope (and records hook name, and timestamp)::
639+
640+ >>> from unitdata import kv
641+ >>> db = kv()
642+ >>> with db.hook_scope('install'):
643+ ... # do work, in transactional scope.
644+ ... db.set('x', 1)
645+ >>> db.get('x')
646+ 1
647+
648+
649+Usage
650+-----
651+
652+Values are automatically json de/serialized to preserve basic typing
653+and complex data struct capabilities (dicts, lists, ints, booleans, etc).
654+
655+Individual values can be manipulated via get/set::
656+
657+ >>> kv.set('y', True)
658+ >>> kv.get('y')
659+ True
660+
661+ # We can set complex values (dicts, lists) as a single key.
662+ >>> kv.set('config', {'a': 1, 'b': True'})
663+
664+ # Also supports returning dictionaries as a record which
665+ # provides attribute access.
666+ >>> config = kv.get('config', record=True)
667+ >>> config.b
668+ True
669+
670+
671+Groups of keys can be manipulated with update/getrange::
672+
673+ >>> kv.update({'z': 1, 'y': 2}, prefix="gui.")
674+ >>> kv.getrange('gui.', strip=True)
675+ {'z': 1, 'y': 2}
676+
677+When updating values, its very helpful to understand which values
678+have actually changed and how have they changed. The storage
679+provides a delta method to provide for this::
680+
681+ >>> data = {'debug': True, 'option': 2}
682+ >>> delta = kv.delta(data, 'config.')
683+ >>> delta.debug.previous
684+ None
685+ >>> delta.debug.current
686+ True
687+ >>> delta
688+ {'debug': (None, True), 'option': (None, 2)}
689+
690+Note the delta method does not persist the actual change, it needs to
691+be explicitly saved via 'update' method::
692+
693+ >>> kv.update(data, 'config.')
694+
695+Values modified in the context of a hook scope retain historical values
696+associated to the hookname.
697+
698+ >>> with db.hook_scope('config-changed'):
699+ ... db.set('x', 42)
700+ >>> db.gethistory('x')
701+ [(1, u'x', 1, u'install', u'2015-01-21T16:49:30.038372'),
702+ (2, u'x', 42, u'config-changed', u'2015-01-21T16:49:30.038786')]
703+
704+"""
705+
706+import collections
707+import contextlib
708+import datetime
709+import json
710+import os
711+import pprint
712+import sqlite3
713+import sys
714+
715+__author__ = 'Kapil Thangavelu <kapil.foss@gmail.com>'
716+
717+
718+class Storage(object):
719+ """Simple key value database for local unit state within charms.
720+
721+ Modifications are automatically committed at hook exit. That's
722+ currently regardless of exit code.
723+
724+ To support dicts, lists, integer, floats, and booleans values
725+ are automatically json encoded/decoded.
726+ """
727+ def __init__(self, path=None):
728+ self.db_path = path
729+ if path is None:
730+ self.db_path = os.path.join(
731+ os.environ.get('CHARM_DIR', ''), '.unit-state.db')
732+ self.conn = sqlite3.connect('%s' % self.db_path)
733+ self.cursor = self.conn.cursor()
734+ self.revision = None
735+ self._closed = False
736+ self._init()
737+
738+ def close(self):
739+ if self._closed:
740+ return
741+ self.flush(False)
742+ self.cursor.close()
743+ self.conn.close()
744+ self._closed = True
745+
746+ def _scoped_query(self, stmt, params=None):
747+ if params is None:
748+ params = []
749+ return stmt, params
750+
751+ def get(self, key, default=None, record=False):
752+ self.cursor.execute(
753+ *self._scoped_query(
754+ 'select data from kv where key=?', [key]))
755+ result = self.cursor.fetchone()
756+ if not result:
757+ return default
758+ if record:
759+ return Record(json.loads(result[0]))
760+ return json.loads(result[0])
761+
762+ def getrange(self, key_prefix, strip=False):
763+ stmt = "select key, data from kv where key like '%s%%'" % key_prefix
764+ self.cursor.execute(*self._scoped_query(stmt))
765+ result = self.cursor.fetchall()
766+
767+ if not result:
768+ return None
769+ if not strip:
770+ key_prefix = ''
771+ return dict([
772+ (k[len(key_prefix):], json.loads(v)) for k, v in result])
773+
774+ def update(self, mapping, prefix=""):
775+ for k, v in mapping.items():
776+ self.set("%s%s" % (prefix, k), v)
777+
778+ def unset(self, key):
779+ self.cursor.execute('delete from kv where key=?', [key])
780+ if self.revision and self.cursor.rowcount:
781+ self.cursor.execute(
782+ 'insert into kv_revisions values (?, ?, ?)',
783+ [key, self.revision, json.dumps('DELETED')])
784+
785+ def set(self, key, value):
786+ serialized = json.dumps(value)
787+
788+ self.cursor.execute(
789+ 'select data from kv where key=?', [key])
790+ exists = self.cursor.fetchone()
791+
792+ # Skip mutations to the same value
793+ if exists:
794+ if exists[0] == serialized:
795+ return value
796+
797+ if not exists:
798+ self.cursor.execute(
799+ 'insert into kv (key, data) values (?, ?)',
800+ (key, serialized))
801+ else:
802+ self.cursor.execute('''
803+ update kv
804+ set data = ?
805+ where key = ?''', [serialized, key])
806+
807+ # Save
808+ if not self.revision:
809+ return value
810+
811+ self.cursor.execute(
812+ 'select 1 from kv_revisions where key=? and revision=?',
813+ [key, self.revision])
814+ exists = self.cursor.fetchone()
815+
816+ if not exists:
817+ self.cursor.execute(
818+ '''insert into kv_revisions (
819+ revision, key, data) values (?, ?, ?)''',
820+ (self.revision, key, serialized))
821+ else:
822+ self.cursor.execute(
823+ '''
824+ update kv_revisions
825+ set data = ?
826+ where key = ?
827+ and revision = ?''',
828+ [serialized, key, self.revision])
829+
830+ return value
831+
832+ def delta(self, mapping, prefix):
833+ """
834+ return a delta containing values that have changed.
835+ """
836+ previous = self.getrange(prefix, strip=True)
837+ if not previous:
838+ pk = set()
839+ else:
840+ pk = set(previous.keys())
841+ ck = set(mapping.keys())
842+ delta = DeltaSet()
843+
844+ # added
845+ for k in ck.difference(pk):
846+ delta[k] = Delta(None, mapping[k])
847+
848+ # removed
849+ for k in pk.difference(ck):
850+ delta[k] = Delta(previous[k], None)
851+
852+ # changed
853+ for k in pk.intersection(ck):
854+ c = mapping[k]
855+ p = previous[k]
856+ if c != p:
857+ delta[k] = Delta(p, c)
858+
859+ return delta
860+
861+ @contextlib.contextmanager
862+ def hook_scope(self, name=""):
863+ """Scope all future interactions to the current hook execution
864+ revision."""
865+ assert not self.revision
866+ self.cursor.execute(
867+ 'insert into hooks (hook, date) values (?, ?)',
868+ (name or sys.argv[0],
869+ datetime.datetime.utcnow().isoformat()))
870+ self.revision = self.cursor.lastrowid
871+ try:
872+ yield self.revision
873+ self.revision = None
874+ except:
875+ self.flush(False)
876+ self.revision = None
877+ raise
878+ else:
879+ self.flush()
880+
881+ def flush(self, save=True):
882+ if save:
883+ self.conn.commit()
884+ elif self._closed:
885+ return
886+ else:
887+ self.conn.rollback()
888+
889+ def _init(self):
890+ self.cursor.execute('''
891+ create table if not exists kv (
892+ key text,
893+ data text,
894+ primary key (key)
895+ )''')
896+ self.cursor.execute('''
897+ create table if not exists kv_revisions (
898+ key text,
899+ revision integer,
900+ data text,
901+ primary key (key, revision)
902+ )''')
903+ self.cursor.execute('''
904+ create table if not exists hooks (
905+ version integer primary key autoincrement,
906+ hook text,
907+ date text
908+ )''')
909+ self.conn.commit()
910+
911+ def gethistory(self, key, deserialize=False):
912+ self.cursor.execute(
913+ '''
914+ select kv.revision, kv.key, kv.data, h.hook, h.date
915+ from kv_revisions kv,
916+ hooks h
917+ where kv.key=?
918+ and kv.revision = h.version
919+ ''', [key])
920+ if deserialize is False:
921+ return self.cursor.fetchall()
922+ return map(_parse_history, self.cursor.fetchall())
923+
924+ def debug(self, fh=sys.stderr):
925+ self.cursor.execute('select * from kv')
926+ pprint.pprint(self.cursor.fetchall(), stream=fh)
927+ self.cursor.execute('select * from kv_revisions')
928+ pprint.pprint(self.cursor.fetchall(), stream=fh)
929+
930+
931+def _parse_history(d):
932+ return (d[0], d[1], json.loads(d[2]), d[3],
933+ datetime.datetime.strptime(d[-1], "%Y-%m-%dT%H:%M:%S.%f"))
934+
935+
936+class HookData(object):
937+ """Simple integration for existing hook exec frameworks.
938+
939+ Records all unit information, and stores deltas for processing
940+ by the hook.
941+
942+ Sample::
943+
944+ from charmhelper.core import hookenv, unitdata
945+
946+ changes = unitdata.HookData()
947+ db = unitdata.kv()
948+ hooks = hookenv.Hooks()
949+
950+ @hooks.hook
951+ def config_changed():
952+ # View all changes to configuration
953+ for changed, (prev, cur) in changes.conf.items():
954+ print('config changed', changed,
955+ 'previous value', prev,
956+ 'current value', cur)
957+
958+ # Get some unit specific bookeeping
959+ if not db.get('pkg_key'):
960+ key = urllib.urlopen('https://example.com/pkg_key').read()
961+ db.set('pkg_key', key)
962+
963+ if __name__ == '__main__':
964+ with changes():
965+ hook.execute()
966+
967+ """
968+ def __init__(self):
969+ self.kv = kv()
970+ self.conf = None
971+ self.rels = None
972+
973+ @contextlib.contextmanager
974+ def __call__(self):
975+ from charmhelpers.core import hookenv
976+ hook_name = hookenv.hook_name()
977+
978+ with self.kv.hook_scope(hook_name):
979+ self._record_charm_version(hookenv.charm_dir())
980+ delta_config, delta_relation = self._record_hook(hookenv)
981+ yield self.kv, delta_config, delta_relation
982+
983+ def _record_charm_version(self, charm_dir):
984+ # Record revisions.. charm revisions are meaningless
985+ # to charm authors as they don't control the revision.
986+ # so logic dependnent on revision is not particularly
987+ # useful, however it is useful for debugging analysis.
988+ charm_rev = open(
989+ os.path.join(charm_dir, 'revision')).read().strip()
990+ charm_rev = charm_rev or '0'
991+ revs = self.kv.get('charm_revisions', [])
992+ if charm_rev not in revs:
993+ revs.append(charm_rev.strip() or '0')
994+ self.kv.set('charm_revisions', revs)
995+
996+ def _record_hook(self, hookenv):
997+ data = hookenv.execution_environment()
998+ self.conf = conf_delta = self.kv.delta(data['conf'], 'config')
999+ self.rels = rels_delta = self.kv.delta(data['rels'], 'rels')
1000+ self.kv.set('env', data['env'])
1001+ self.kv.set('unit', data['unit'])
1002+ self.kv.set('relid', data.get('relid'))
1003+ return conf_delta, rels_delta
1004+
1005+
1006+class Record(dict):
1007+
1008+ __slots__ = ()
1009+
1010+ def __getattr__(self, k):
1011+ if k in self:
1012+ return self[k]
1013+ raise AttributeError(k)
1014+
1015+
1016+class DeltaSet(Record):
1017+
1018+ __slots__ = ()
1019+
1020+
1021+Delta = collections.namedtuple('Delta', ['previous', 'current'])
1022+
1023+
1024+_KV = None
1025+
1026+
1027+def kv():
1028+ global _KV
1029+ if _KV is None:
1030+ _KV = Storage()
1031+ return _KV
1032
1033=== modified file 'hooks/charmhelpers/fetch/archiveurl.py'
1034--- hooks/charmhelpers/fetch/archiveurl.py 2015-01-26 09:48:14 +0000
1035+++ hooks/charmhelpers/fetch/archiveurl.py 2015-02-24 06:29:10 +0000
1036@@ -18,6 +18,16 @@
1037 import hashlib
1038 import re
1039
1040+from charmhelpers.fetch import (
1041+ BaseFetchHandler,
1042+ UnhandledSource
1043+)
1044+from charmhelpers.payload.archive import (
1045+ get_archive_handler,
1046+ extract,
1047+)
1048+from charmhelpers.core.host import mkdir, check_hash
1049+
1050 import six
1051 if six.PY3:
1052 from urllib.request import (
1053@@ -35,16 +45,6 @@
1054 )
1055 from urlparse import urlparse, urlunparse, parse_qs
1056
1057-from charmhelpers.fetch import (
1058- BaseFetchHandler,
1059- UnhandledSource
1060-)
1061-from charmhelpers.payload.archive import (
1062- get_archive_handler,
1063- extract,
1064-)
1065-from charmhelpers.core.host import mkdir, check_hash
1066-
1067
1068 def splituser(host):
1069 '''urllib.splituser(), but six's support of this seems broken'''
1070
1071=== modified file 'hooks/charmhelpers/fetch/giturl.py'
1072--- hooks/charmhelpers/fetch/giturl.py 2015-01-26 09:48:14 +0000
1073+++ hooks/charmhelpers/fetch/giturl.py 2015-02-24 06:29:10 +0000
1074@@ -32,7 +32,7 @@
1075 apt_install("python-git")
1076 from git import Repo
1077
1078-from git.exc import GitCommandError
1079+from git.exc import GitCommandError # noqa E402
1080
1081
1082 class GitUrlFetchHandler(BaseFetchHandler):

Subscribers

People subscribed via source and target branches