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

Proposed by Brad Marshall
Status: Superseded
Proposed branch: lp:~brad-marshall/charms/trusty/glance/add-haproxy-nrpe-fix-servicegroups
Merge into: lp:~openstack-charmers-archive/charms/trusty/glance/trunk
Diff against target: 1101 lines (+738/-43)
20 files modified
config.yaml (+6/-0)
hooks/charmhelpers/contrib/charmsupport/nrpe.py (+40/-6)
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)
hooks/glance_relations.py (+2/-0)
tests/charmhelpers/contrib/openstack/amulet/deployment.py (+5/-2)
To merge this branch: bzr merge lp:~brad-marshall/charms/trusty/glance/add-haproxy-nrpe-fix-servicegroups
Reviewer Review Type Date Requested Status
OpenStack Charmers Pending
Review via email: mp+250240@code.launchpad.net

This proposal has been superseded by a proposal from 2015-02-19.

Description of the change

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

To post a comment you must log in.
102. By Brad Marshall

[bradm] Handle case of empty nagios_servicegroups setting

Unmerged revisions

97. By Corey Bryant

[coreycb,trivial] Switch Amulet tests to use stable branches

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

Subscribers

People subscribed via source and target branches