Merge lp:~brad-marshall/charms/trusty/rabbitmq-server/nagios-fixes-sync-charmhelpers into lp:charms/trusty/rabbitmq-server

Proposed by Brad Marshall
Status: Merged
Merged at revision: 85
Proposed branch: lp:~brad-marshall/charms/trusty/rabbitmq-server/nagios-fixes-sync-charmhelpers
Merge into: lp:charms/trusty/rabbitmq-server
Diff against target: 1626 lines (+1012/-144)
29 files modified
charm-helpers.yaml (+1/-1)
config.yaml (+6/-0)
hooks/charmhelpers/contrib/charmsupport/nrpe.py (+41/-7)
hooks/charmhelpers/contrib/hahelpers/cluster.py (+5/-1)
hooks/charmhelpers/contrib/network/ip.py (+84/-1)
hooks/charmhelpers/contrib/openstack/amulet/deployment.py (+5/-2)
hooks/charmhelpers/contrib/openstack/context.py (+29/-10)
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/templates/ceph.conf (+15/-0)
hooks/charmhelpers/contrib/openstack/templates/haproxy.cfg (+58/-0)
hooks/charmhelpers/contrib/openstack/templates/openstack_https_frontend (+24/-0)
hooks/charmhelpers/contrib/openstack/templates/openstack_https_frontend.conf (+24/-0)
hooks/charmhelpers/contrib/openstack/templates/zeromq (+14/-0)
hooks/charmhelpers/contrib/openstack/utils.py (+7/-72)
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/services/helpers.py (+12/-4)
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/rabbit_utils.py (+8/-0)
hooks/rabbitmq_server_relations.py (+5/-14)
To merge this branch: bzr merge lp:~brad-marshall/charms/trusty/rabbitmq-server/nagios-fixes-sync-charmhelpers
Reviewer Review Type Date Requested Status
Marco Ceppi (community) Approve
Adam Israel (community) Approve
Review Queue (community) automated testing Needs Fixing
OpenStack Charmers Pending
Review via email: mp+251551@code.launchpad.net

Description of the change

Add nagios-servicegroup config option, add nrpe check for service

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

charm_unit_test #2183 rabbitmq-server for brad-marshall mp251551
    UNIT OK: passed

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

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

charm_lint_check #2393 rabbitmq-server for brad-marshall mp251551
    LINT FAIL: lint-test failed

LINT Results (max last 2 lines):
  unit_tests/test_rabbitmq_server_relations.py:6:1: E402 module level import not at top of file
  make: *** [lint] Error 1

Full lint test output: http://paste.ubuntu.com/10520218/
Build: http://10.245.162.77:8080/job/charm_lint_check/2393/

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

charm_amulet_test #2313 rabbitmq-server for brad-marshall mp251551
    AMULET FAIL: amulet-test failed

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

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

86. By Brad Marshall

[bradm] Remove init nrpe checks, something isn't quite right with them for the checks.

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

charm_lint_check #2491 rabbitmq-server for brad-marshall mp251551
    LINT FAIL: lint-test failed

LINT Results (max last 2 lines):
  unit_tests/test_rabbitmq_server_relations.py:6:1: E402 module level import not at top of file
  make: *** [lint] Error 1

Full lint test output: http://paste.ubuntu.com/10553544/
Build: http://10.245.162.77:8080/job/charm_lint_check/2491/

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

charm_amulet_test #2363 rabbitmq-server for brad-marshall mp251551
    AMULET FAIL: amulet-test failed

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

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

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

charm_unit_test #2281 rabbitmq-server for brad-marshall mp251551
    UNIT OK: passed

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

Revision history for this message
Review Queue (review-queue) wrote :

This items has failed automated testing! Results available here http://reports.vapour.ws/charm-tests/charm-bundle-test-11088-results

review: Needs Fixing (automated testing)
Revision history for this message
Antonio Rosales (arosales) wrote :

Running /tmp/bundletester-Ts1jVN/rabbitmq-server/tests/10_basic_deploy_test.py on AWS it looks like SSL cert verification is failing:

Testing ssl connection to rabbitmq-server.
Failed to create an ssl connection to 54.173.110.197:5671
[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed (_ssl.c:598)

This also appears to be occurring across the other substrates.

@Brad,
Are you seeing this in your testing also?

-thanks,
Antonio

Revision history for this message
Adam Israel (aisrael) wrote :

Hi Brad,

Thanks for your work on improving the rabbitmq-server charm. I reviewed the merge proposal and your changes look solid. There are issues with the unit tests upstream, and bugs have been opened against those. I don't see anything in this MP that would caused the test failures, so you have a +1 from me.

review: Approve
Revision history for this message
Marco Ceppi (marcoceppi) wrote :

LGTM +1

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'charm-helpers.yaml'
2--- charm-helpers.yaml 2015-01-23 08:23:05 +0000
3+++ charm-helpers.yaml 2015-03-06 06:41:14 +0000
4@@ -4,7 +4,7 @@
5 - fetch
6 - core
7 - contrib.charmsupport
8- - contrib.openstack
9+ - contrib.openstack|inc=*
10 - contrib.storage
11 - contrib.peerstorage
12 - contrib.python.packages
13
14=== modified file 'config.yaml'
15--- config.yaml 2015-01-23 08:28:27 +0000
16+++ config.yaml 2015-03-06 06:41:14 +0000
17@@ -45,6 +45,12 @@
18 juju-myservice-0
19 If you're running multiple environments with the same services in them
20 this allows you to differentiate between them.
21+ nagios_servicegroups:
22+ default: ""
23+ type: string
24+ description: |
25+ A comma-separated list of nagios servicegroups.
26+ If left empty, the nagios_context will be used as the servicegroup
27 # HA configuration settings
28 vip:
29 type: string
30
31=== modified file 'hooks/charmhelpers/contrib/charmsupport/nrpe.py'
32--- hooks/charmhelpers/contrib/charmsupport/nrpe.py 2015-01-26 09:45:59 +0000
33+++ hooks/charmhelpers/contrib/charmsupport/nrpe.py 2015-03-06 06:41:14 +0000
34@@ -24,6 +24,8 @@
35 import pwd
36 import grp
37 import os
38+import glob
39+import shutil
40 import re
41 import shlex
42 import yaml
43@@ -161,7 +163,7 @@
44 log('Check command not found: {}'.format(parts[0]))
45 return ''
46
47- def write(self, nagios_context, hostname, nagios_servicegroups=None):
48+ def write(self, nagios_context, hostname, nagios_servicegroups):
49 nrpe_check_file = '/etc/nagios/nrpe.d/{}.cfg'.format(
50 self.command)
51 with open(nrpe_check_file, 'w') as nrpe_check_config:
52@@ -177,14 +179,11 @@
53 nagios_servicegroups)
54
55 def write_service_config(self, nagios_context, hostname,
56- nagios_servicegroups=None):
57+ nagios_servicegroups):
58 for f in os.listdir(NRPE.nagios_exportdir):
59 if re.search('.*{}.cfg'.format(self.command), f):
60 os.remove(os.path.join(NRPE.nagios_exportdir, f))
61
62- if not nagios_servicegroups:
63- nagios_servicegroups = nagios_context
64-
65 templ_vars = {
66 'nagios_hostname': hostname,
67 'nagios_servicegroup': nagios_servicegroups,
68@@ -211,10 +210,10 @@
69 super(NRPE, self).__init__()
70 self.config = config()
71 self.nagios_context = self.config['nagios_context']
72- if 'nagios_servicegroups' in self.config:
73+ if 'nagios_servicegroups' in self.config and self.config['nagios_servicegroups']:
74 self.nagios_servicegroups = self.config['nagios_servicegroups']
75 else:
76- self.nagios_servicegroups = 'juju'
77+ self.nagios_servicegroups = self.nagios_context
78 self.unit_name = local_unit().replace('/', '-')
79 if hostname:
80 self.hostname = hostname
81@@ -322,3 +321,38 @@
82 check_cmd='check_status_file.py -f '
83 '/var/lib/nagios/service-check-%s.txt' % svc,
84 )
85+
86+
87+def copy_nrpe_checks():
88+ """
89+ Copy the nrpe checks into place
90+
91+ """
92+ NAGIOS_PLUGINS = '/usr/local/lib/nagios/plugins'
93+ nrpe_files_dir = os.path.join(os.getenv('CHARM_DIR'), 'hooks',
94+ 'charmhelpers', 'contrib', 'openstack',
95+ 'files')
96+
97+ if not os.path.exists(NAGIOS_PLUGINS):
98+ os.makedirs(NAGIOS_PLUGINS)
99+ for fname in glob.glob(os.path.join(nrpe_files_dir, "check_*")):
100+ if os.path.isfile(fname):
101+ shutil.copy2(fname,
102+ os.path.join(NAGIOS_PLUGINS, os.path.basename(fname)))
103+
104+
105+def add_haproxy_checks(nrpe, unit_name):
106+ """
107+ Add checks for each service in list
108+
109+ :param NRPE nrpe: NRPE object to add check to
110+ :param str unit_name: Unit name to use in check description
111+ """
112+ nrpe.add_check(
113+ shortname='haproxy_servers',
114+ description='Check HAProxy {%s}' % unit_name,
115+ check_cmd='check_haproxy.sh')
116+ nrpe.add_check(
117+ shortname='haproxy_queue',
118+ description='Check HAProxy queue depth {%s}' % unit_name,
119+ check_cmd='check_haproxy_queue_depth.sh')
120
121=== modified file 'hooks/charmhelpers/contrib/hahelpers/cluster.py'
122--- hooks/charmhelpers/contrib/hahelpers/cluster.py 2015-01-26 09:45:59 +0000
123+++ hooks/charmhelpers/contrib/hahelpers/cluster.py 2015-03-06 06:41:14 +0000
124@@ -48,6 +48,9 @@
125 from charmhelpers.core.decorators import (
126 retry_on_exception,
127 )
128+from charmhelpers.core.strutils import (
129+ bool_from_string,
130+)
131
132
133 class HAIncompleteConfig(Exception):
134@@ -164,7 +167,8 @@
135 .
136 returns: boolean
137 '''
138- if config_get('use-https') == "yes":
139+ use_https = config_get('use-https')
140+ if use_https and bool_from_string(use_https):
141 return True
142 if config_get('ssl_cert') and config_get('ssl_key'):
143 return True
144
145=== modified file 'hooks/charmhelpers/contrib/network/ip.py'
146--- hooks/charmhelpers/contrib/network/ip.py 2015-01-26 09:45:59 +0000
147+++ hooks/charmhelpers/contrib/network/ip.py 2015-03-06 06:41:14 +0000
148@@ -17,13 +17,16 @@
149 import glob
150 import re
151 import subprocess
152+import six
153+import socket
154
155 from functools import partial
156
157 from charmhelpers.core.hookenv import unit_get
158 from charmhelpers.fetch import apt_install
159 from charmhelpers.core.hookenv import (
160- log
161+ log,
162+ WARNING,
163 )
164
165 try:
166@@ -365,3 +368,83 @@
167 return True
168
169 return False
170+
171+
172+def is_ip(address):
173+ """
174+ Returns True if address is a valid IP address.
175+ """
176+ try:
177+ # Test to see if already an IPv4 address
178+ socket.inet_aton(address)
179+ return True
180+ except socket.error:
181+ return False
182+
183+
184+def ns_query(address):
185+ try:
186+ import dns.resolver
187+ except ImportError:
188+ apt_install('python-dnspython')
189+ import dns.resolver
190+
191+ if isinstance(address, dns.name.Name):
192+ rtype = 'PTR'
193+ elif isinstance(address, six.string_types):
194+ rtype = 'A'
195+ else:
196+ return None
197+
198+ answers = dns.resolver.query(address, rtype)
199+ if answers:
200+ return str(answers[0])
201+ return None
202+
203+
204+def get_host_ip(hostname, fallback=None):
205+ """
206+ Resolves the IP for a given hostname, or returns
207+ the input if it is already an IP.
208+ """
209+ if is_ip(hostname):
210+ return hostname
211+
212+ ip_addr = ns_query(hostname)
213+ if not ip_addr:
214+ try:
215+ ip_addr = socket.gethostbyname(hostname)
216+ except:
217+ log("Failed to resolve hostname '%s'" % (hostname),
218+ level=WARNING)
219+ return fallback
220+ return ip_addr
221+
222+
223+def get_hostname(address, fqdn=True):
224+ """
225+ Resolves hostname for given IP, or returns the input
226+ if it is already a hostname.
227+ """
228+ if is_ip(address):
229+ try:
230+ import dns.reversename
231+ except ImportError:
232+ apt_install("python-dnspython")
233+ import dns.reversename
234+
235+ rev = dns.reversename.from_address(address)
236+ result = ns_query(rev)
237+ if not result:
238+ return None
239+ else:
240+ result = address
241+
242+ if fqdn:
243+ # strip trailing .
244+ if result.endswith('.'):
245+ return result[:-1]
246+ else:
247+ return result
248+ else:
249+ return result.split('.')[0]
250
251=== modified file 'hooks/charmhelpers/contrib/openstack/amulet/deployment.py'
252--- hooks/charmhelpers/contrib/openstack/amulet/deployment.py 2015-01-26 09:45:59 +0000
253+++ hooks/charmhelpers/contrib/openstack/amulet/deployment.py 2015-03-06 06:41:14 +0000
254@@ -71,16 +71,19 @@
255 services.append(this_service)
256 use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph',
257 'ceph-osd', 'ceph-radosgw']
258+ # Openstack subordinate charms do not expose an origin option as that
259+ # is controlled by the principle
260+ ignore = ['neutron-openvswitch']
261
262 if self.openstack:
263 for svc in services:
264- if svc['name'] not in use_source:
265+ if svc['name'] not in use_source + ignore:
266 config = {'openstack-origin': self.openstack}
267 self.d.configure(svc['name'], config)
268
269 if self.source:
270 for svc in services:
271- if svc['name'] in use_source:
272+ if svc['name'] in use_source and svc['name'] not in ignore:
273 config = {'source': self.source}
274 self.d.configure(svc['name'], config)
275
276
277=== modified file 'hooks/charmhelpers/contrib/openstack/context.py'
278--- hooks/charmhelpers/contrib/openstack/context.py 2015-01-26 09:45:59 +0000
279+++ hooks/charmhelpers/contrib/openstack/context.py 2015-03-06 06:41:14 +0000
280@@ -191,7 +191,7 @@
281 unit=local_unit())
282 if set_hostname != access_hostname:
283 relation_set(relation_settings={hostname_key: access_hostname})
284- return ctxt # Defer any further hook execution for now....
285+ return None # Defer any further hook execution for now....
286
287 password_setting = 'password'
288 if self.relation_prefix:
289@@ -279,9 +279,25 @@
290 class IdentityServiceContext(OSContextGenerator):
291 interfaces = ['identity-service']
292
293+ def __init__(self, service=None, service_user=None):
294+ self.service = service
295+ self.service_user = service_user
296+
297 def __call__(self):
298 log('Generating template context for identity-service', level=DEBUG)
299 ctxt = {}
300+
301+ if self.service and self.service_user:
302+ # This is required for pki token signing if we don't want /tmp to
303+ # be used.
304+ cachedir = '/var/cache/%s' % (self.service)
305+ if not os.path.isdir(cachedir):
306+ log("Creating service cache dir %s" % (cachedir), level=DEBUG)
307+ mkdir(path=cachedir, owner=self.service_user,
308+ group=self.service_user, perms=0o700)
309+
310+ ctxt['signing_dir'] = cachedir
311+
312 for rid in relation_ids('identity-service'):
313 for unit in related_units(rid):
314 rdata = relation_get(rid=rid, unit=unit)
315@@ -291,15 +307,16 @@
316 auth_host = format_ipv6_addr(auth_host) or auth_host
317 svc_protocol = rdata.get('service_protocol') or 'http'
318 auth_protocol = rdata.get('auth_protocol') or 'http'
319- ctxt = {'service_port': rdata.get('service_port'),
320- 'service_host': serv_host,
321- 'auth_host': auth_host,
322- 'auth_port': rdata.get('auth_port'),
323- 'admin_tenant_name': rdata.get('service_tenant'),
324- 'admin_user': rdata.get('service_username'),
325- 'admin_password': rdata.get('service_password'),
326- 'service_protocol': svc_protocol,
327- 'auth_protocol': auth_protocol}
328+ ctxt.update({'service_port': rdata.get('service_port'),
329+ 'service_host': serv_host,
330+ 'auth_host': auth_host,
331+ 'auth_port': rdata.get('auth_port'),
332+ 'admin_tenant_name': rdata.get('service_tenant'),
333+ 'admin_user': rdata.get('service_username'),
334+ 'admin_password': rdata.get('service_password'),
335+ 'service_protocol': svc_protocol,
336+ 'auth_protocol': auth_protocol})
337+
338 if context_complete(ctxt):
339 # NOTE(jamespage) this is required for >= icehouse
340 # so a missing value just indicates keystone needs
341@@ -1021,6 +1038,8 @@
342 for unit in related_units(rid):
343 ctxt['zmq_nonce'] = relation_get('nonce', unit, rid)
344 ctxt['zmq_host'] = relation_get('host', unit, rid)
345+ ctxt['zmq_redis_address'] = relation_get(
346+ 'zmq_redis_address', unit, rid)
347
348 return ctxt
349
350
351=== added directory 'hooks/charmhelpers/contrib/openstack/files'
352=== added file 'hooks/charmhelpers/contrib/openstack/files/__init__.py'
353--- hooks/charmhelpers/contrib/openstack/files/__init__.py 1970-01-01 00:00:00 +0000
354+++ hooks/charmhelpers/contrib/openstack/files/__init__.py 2015-03-06 06:41:14 +0000
355@@ -0,0 +1,18 @@
356+# Copyright 2014-2015 Canonical Limited.
357+#
358+# This file is part of charm-helpers.
359+#
360+# charm-helpers is free software: you can redistribute it and/or modify
361+# it under the terms of the GNU Lesser General Public License version 3 as
362+# published by the Free Software Foundation.
363+#
364+# charm-helpers is distributed in the hope that it will be useful,
365+# but WITHOUT ANY WARRANTY; without even the implied warranty of
366+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
367+# GNU Lesser General Public License for more details.
368+#
369+# You should have received a copy of the GNU Lesser General Public License
370+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
371+
372+# dummy __init__.py to fool syncer into thinking this is a syncable python
373+# module
374
375=== added file 'hooks/charmhelpers/contrib/openstack/files/check_haproxy.sh'
376--- hooks/charmhelpers/contrib/openstack/files/check_haproxy.sh 1970-01-01 00:00:00 +0000
377+++ hooks/charmhelpers/contrib/openstack/files/check_haproxy.sh 2015-03-06 06:41:14 +0000
378@@ -0,0 +1,32 @@
379+#!/bin/bash
380+#--------------------------------------------
381+# This file is managed by Juju
382+#--------------------------------------------
383+#
384+# Copyright 2009,2012 Canonical Ltd.
385+# Author: Tom Haddon
386+
387+CRITICAL=0
388+NOTACTIVE=''
389+LOGFILE=/var/log/nagios/check_haproxy.log
390+AUTH=$(grep -r "stats auth" /etc/haproxy | head -1 | awk '{print $4}')
391+
392+for appserver in $(grep ' server' /etc/haproxy/haproxy.cfg | awk '{print $2'});
393+do
394+ 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')
395+ if [ $? != 0 ]; then
396+ date >> $LOGFILE
397+ echo $output >> $LOGFILE
398+ /usr/lib/nagios/plugins/check_http -a ${AUTH} -I 127.0.0.1 -p 8888 -v | grep $appserver >> $LOGFILE 2>&1
399+ CRITICAL=1
400+ NOTACTIVE="${NOTACTIVE} $appserver"
401+ fi
402+done
403+
404+if [ $CRITICAL = 1 ]; then
405+ echo "CRITICAL:${NOTACTIVE}"
406+ exit 2
407+fi
408+
409+echo "OK: All haproxy instances looking good"
410+exit 0
411
412=== added file 'hooks/charmhelpers/contrib/openstack/files/check_haproxy_queue_depth.sh'
413--- hooks/charmhelpers/contrib/openstack/files/check_haproxy_queue_depth.sh 1970-01-01 00:00:00 +0000
414+++ hooks/charmhelpers/contrib/openstack/files/check_haproxy_queue_depth.sh 2015-03-06 06:41:14 +0000
415@@ -0,0 +1,30 @@
416+#!/bin/bash
417+#--------------------------------------------
418+# This file is managed by Juju
419+#--------------------------------------------
420+#
421+# Copyright 2009,2012 Canonical Ltd.
422+# Author: Tom Haddon
423+
424+# These should be config options at some stage
425+CURRQthrsh=0
426+MAXQthrsh=100
427+
428+AUTH=$(grep -r "stats auth" /etc/haproxy | head -1 | awk '{print $4}')
429+
430+HAPROXYSTATS=$(/usr/lib/nagios/plugins/check_http -a ${AUTH} -I 127.0.0.1 -p 8888 -u '/;csv' -v)
431+
432+for BACKEND in $(echo $HAPROXYSTATS| xargs -n1 | grep BACKEND | awk -F , '{print $1}')
433+do
434+ CURRQ=$(echo "$HAPROXYSTATS" | grep $BACKEND | grep BACKEND | cut -d , -f 3)
435+ MAXQ=$(echo "$HAPROXYSTATS" | grep $BACKEND | grep BACKEND | cut -d , -f 4)
436+
437+ if [[ $CURRQ -gt $CURRQthrsh || $MAXQ -gt $MAXQthrsh ]] ; then
438+ echo "CRITICAL: queue depth for $BACKEND - CURRENT:$CURRQ MAX:$MAXQ"
439+ exit 2
440+ fi
441+done
442+
443+echo "OK: All haproxy queue depths looking good"
444+exit 0
445+
446
447=== modified file 'hooks/charmhelpers/contrib/openstack/ip.py'
448--- hooks/charmhelpers/contrib/openstack/ip.py 2015-01-26 09:45:59 +0000
449+++ hooks/charmhelpers/contrib/openstack/ip.py 2015-03-06 06:41:14 +0000
450@@ -26,6 +26,8 @@
451 )
452 from charmhelpers.contrib.hahelpers.cluster import is_clustered
453
454+from functools import partial
455+
456 PUBLIC = 'public'
457 INTERNAL = 'int'
458 ADMIN = 'admin'
459@@ -107,3 +109,38 @@
460 "clustered=%s)" % (net_type, clustered))
461
462 return resolved_address
463+
464+
465+def endpoint_url(configs, url_template, port, endpoint_type=PUBLIC,
466+ override=None):
467+ """Returns the correct endpoint URL to advertise to Keystone.
468+
469+ This method provides the correct endpoint URL which should be advertised to
470+ the keystone charm for endpoint creation. This method allows for the url to
471+ be overridden to force a keystone endpoint to have specific URL for any of
472+ the defined scopes (admin, internal, public).
473+
474+ :param configs: OSTemplateRenderer config templating object to inspect
475+ for a complete https context.
476+ :param url_template: str format string for creating the url template. Only
477+ two values will be passed - the scheme+hostname
478+ returned by the canonical_url and the port.
479+ :param endpoint_type: str endpoint type to resolve.
480+ :param override: str the name of the config option which overrides the
481+ endpoint URL defined by the charm itself. None will
482+ disable any overrides (default).
483+ """
484+ if override:
485+ # Return any user-defined overrides for the keystone endpoint URL.
486+ user_value = config(override)
487+ if user_value:
488+ return user_value.strip()
489+
490+ return url_template % (canonical_url(configs, endpoint_type), port)
491+
492+
493+public_endpoint = partial(endpoint_url, endpoint_type=PUBLIC)
494+
495+internal_endpoint = partial(endpoint_url, endpoint_type=INTERNAL)
496+
497+admin_endpoint = partial(endpoint_url, endpoint_type=ADMIN)
498
499=== added file 'hooks/charmhelpers/contrib/openstack/templates/ceph.conf'
500--- hooks/charmhelpers/contrib/openstack/templates/ceph.conf 1970-01-01 00:00:00 +0000
501+++ hooks/charmhelpers/contrib/openstack/templates/ceph.conf 2015-03-06 06:41:14 +0000
502@@ -0,0 +1,15 @@
503+###############################################################################
504+# [ WARNING ]
505+# cinder configuration file maintained by Juju
506+# local changes may be overwritten.
507+###############################################################################
508+[global]
509+{% if auth -%}
510+ auth_supported = {{ auth }}
511+ keyring = /etc/ceph/$cluster.$name.keyring
512+ mon host = {{ mon_hosts }}
513+{% endif -%}
514+ log to syslog = {{ use_syslog }}
515+ err to syslog = {{ use_syslog }}
516+ clog to syslog = {{ use_syslog }}
517+
518
519=== added file 'hooks/charmhelpers/contrib/openstack/templates/haproxy.cfg'
520--- hooks/charmhelpers/contrib/openstack/templates/haproxy.cfg 1970-01-01 00:00:00 +0000
521+++ hooks/charmhelpers/contrib/openstack/templates/haproxy.cfg 2015-03-06 06:41:14 +0000
522@@ -0,0 +1,58 @@
523+global
524+ log {{ local_host }} local0
525+ log {{ local_host }} local1 notice
526+ maxconn 20000
527+ user haproxy
528+ group haproxy
529+ spread-checks 0
530+
531+defaults
532+ log global
533+ mode tcp
534+ option tcplog
535+ option dontlognull
536+ retries 3
537+ timeout queue 1000
538+ timeout connect 1000
539+{% if haproxy_client_timeout -%}
540+ timeout client {{ haproxy_client_timeout }}
541+{% else -%}
542+ timeout client 30000
543+{% endif -%}
544+
545+{% if haproxy_server_timeout -%}
546+ timeout server {{ haproxy_server_timeout }}
547+{% else -%}
548+ timeout server 30000
549+{% endif -%}
550+
551+listen stats {{ stat_port }}
552+ mode http
553+ stats enable
554+ stats hide-version
555+ stats realm Haproxy\ Statistics
556+ stats uri /
557+ stats auth admin:password
558+
559+{% if frontends -%}
560+{% for service, ports in service_ports.items() -%}
561+frontend tcp-in_{{ service }}
562+ bind *:{{ ports[0] }}
563+ {% if ipv6 -%}
564+ bind :::{{ ports[0] }}
565+ {% endif -%}
566+ {% for frontend in frontends -%}
567+ acl net_{{ frontend }} dst {{ frontends[frontend]['network'] }}
568+ use_backend {{ service }}_{{ frontend }} if net_{{ frontend }}
569+ {% endfor -%}
570+ default_backend {{ service }}_{{ default_backend }}
571+
572+{% for frontend in frontends -%}
573+backend {{ service }}_{{ frontend }}
574+ balance leastconn
575+ {% for unit, address in frontends[frontend]['backends'].items() -%}
576+ server {{ unit }} {{ address }}:{{ ports[1] }} check
577+ {% endfor %}
578+{% endfor -%}
579+{% endfor -%}
580+{% endif -%}
581
582=== added file 'hooks/charmhelpers/contrib/openstack/templates/openstack_https_frontend'
583--- hooks/charmhelpers/contrib/openstack/templates/openstack_https_frontend 1970-01-01 00:00:00 +0000
584+++ hooks/charmhelpers/contrib/openstack/templates/openstack_https_frontend 2015-03-06 06:41:14 +0000
585@@ -0,0 +1,24 @@
586+{% if endpoints -%}
587+{% for ext_port in ext_ports -%}
588+Listen {{ ext_port }}
589+{% endfor -%}
590+{% for address, endpoint, ext, int in endpoints -%}
591+<VirtualHost {{ address }}:{{ ext }}>
592+ ServerName {{ endpoint }}
593+ SSLEngine on
594+ SSLCertificateFile /etc/apache2/ssl/{{ namespace }}/cert_{{ endpoint }}
595+ SSLCertificateKeyFile /etc/apache2/ssl/{{ namespace }}/key_{{ endpoint }}
596+ ProxyPass / http://localhost:{{ int }}/
597+ ProxyPassReverse / http://localhost:{{ int }}/
598+ ProxyPreserveHost on
599+</VirtualHost>
600+{% endfor -%}
601+<Proxy *>
602+ Order deny,allow
603+ Allow from all
604+</Proxy>
605+<Location />
606+ Order allow,deny
607+ Allow from all
608+</Location>
609+{% endif -%}
610
611=== added file 'hooks/charmhelpers/contrib/openstack/templates/openstack_https_frontend.conf'
612--- hooks/charmhelpers/contrib/openstack/templates/openstack_https_frontend.conf 1970-01-01 00:00:00 +0000
613+++ hooks/charmhelpers/contrib/openstack/templates/openstack_https_frontend.conf 2015-03-06 06:41:14 +0000
614@@ -0,0 +1,24 @@
615+{% if endpoints -%}
616+{% for ext_port in ext_ports -%}
617+Listen {{ ext_port }}
618+{% endfor -%}
619+{% for address, endpoint, ext, int in endpoints -%}
620+<VirtualHost {{ address }}:{{ ext }}>
621+ ServerName {{ endpoint }}
622+ SSLEngine on
623+ SSLCertificateFile /etc/apache2/ssl/{{ namespace }}/cert_{{ endpoint }}
624+ SSLCertificateKeyFile /etc/apache2/ssl/{{ namespace }}/key_{{ endpoint }}
625+ ProxyPass / http://localhost:{{ int }}/
626+ ProxyPassReverse / http://localhost:{{ int }}/
627+ ProxyPreserveHost on
628+</VirtualHost>
629+{% endfor -%}
630+<Proxy *>
631+ Order deny,allow
632+ Allow from all
633+</Proxy>
634+<Location />
635+ Order allow,deny
636+ Allow from all
637+</Location>
638+{% endif -%}
639
640=== added file 'hooks/charmhelpers/contrib/openstack/templates/zeromq'
641--- hooks/charmhelpers/contrib/openstack/templates/zeromq 1970-01-01 00:00:00 +0000
642+++ hooks/charmhelpers/contrib/openstack/templates/zeromq 2015-03-06 06:41:14 +0000
643@@ -0,0 +1,14 @@
644+{% if zmq_host -%}
645+# ZeroMQ configuration (restart-nonce: {{ zmq_nonce }})
646+rpc_backend = zmq
647+rpc_zmq_host = {{ zmq_host }}
648+{% if zmq_redis_address -%}
649+rpc_zmq_matchmaker = oslo.messaging._drivers.matchmaker_redis.MatchMakerRedis
650+matchmaker_heartbeat_freq = 15
651+matchmaker_heartbeat_ttl = 30
652+[matchmaker_redis]
653+host = {{ zmq_redis_address }}
654+{% else -%}
655+rpc_zmq_matchmaker = oslo.messaging._drivers.matchmaker_ring.MatchMakerRing
656+{% endif -%}
657+{% endif -%}
658
659=== modified file 'hooks/charmhelpers/contrib/openstack/utils.py'
660--- hooks/charmhelpers/contrib/openstack/utils.py 2015-01-26 09:45:59 +0000
661+++ hooks/charmhelpers/contrib/openstack/utils.py 2015-03-06 06:41:14 +0000
662@@ -23,12 +23,13 @@
663 import subprocess
664 import json
665 import os
666-import socket
667 import sys
668
669 import six
670 import yaml
671
672+from charmhelpers.contrib.network import ip
673+
674 from charmhelpers.core.hookenv import (
675 config,
676 log as juju_log,
677@@ -103,6 +104,7 @@
678 ('2.1.0', 'juno'),
679 ('2.2.0', 'juno'),
680 ('2.2.1', 'kilo'),
681+ ('2.2.2', 'kilo'),
682 ])
683
684 DEFAULT_LOOPBACK_SIZE = '5G'
685@@ -420,77 +422,10 @@
686 else:
687 zap_disk(block_device)
688
689-
690-def is_ip(address):
691- """
692- Returns True if address is a valid IP address.
693- """
694- try:
695- # Test to see if already an IPv4 address
696- socket.inet_aton(address)
697- return True
698- except socket.error:
699- return False
700-
701-
702-def ns_query(address):
703- try:
704- import dns.resolver
705- except ImportError:
706- apt_install('python-dnspython')
707- import dns.resolver
708-
709- if isinstance(address, dns.name.Name):
710- rtype = 'PTR'
711- elif isinstance(address, six.string_types):
712- rtype = 'A'
713- else:
714- return None
715-
716- answers = dns.resolver.query(address, rtype)
717- if answers:
718- return str(answers[0])
719- return None
720-
721-
722-def get_host_ip(hostname):
723- """
724- Resolves the IP for a given hostname, or returns
725- the input if it is already an IP.
726- """
727- if is_ip(hostname):
728- return hostname
729-
730- return ns_query(hostname)
731-
732-
733-def get_hostname(address, fqdn=True):
734- """
735- Resolves hostname for given IP, or returns the input
736- if it is already a hostname.
737- """
738- if is_ip(address):
739- try:
740- import dns.reversename
741- except ImportError:
742- apt_install('python-dnspython')
743- import dns.reversename
744-
745- rev = dns.reversename.from_address(address)
746- result = ns_query(rev)
747- if not result:
748- return None
749- else:
750- result = address
751-
752- if fqdn:
753- # strip trailing .
754- if result.endswith('.'):
755- return result[:-1]
756- else:
757- return result
758- else:
759- return result.split('.')[0]
760+is_ip = ip.is_ip
761+ns_query = ip.ns_query
762+get_host_ip = ip.get_host_ip
763+get_hostname = ip.get_hostname
764
765
766 def get_matchmaker_map(mm_file='/etc/oslo/matchmaker_ring.json'):
767
768=== modified file 'hooks/charmhelpers/contrib/python/packages.py'
769--- hooks/charmhelpers/contrib/python/packages.py 2015-01-26 09:45:59 +0000
770+++ hooks/charmhelpers/contrib/python/packages.py 2015-03-06 06:41:14 +0000
771@@ -17,8 +17,6 @@
772 # You should have received a copy of the GNU Lesser General Public License
773 # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
774
775-__author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>"
776-
777 from charmhelpers.fetch import apt_install, apt_update
778 from charmhelpers.core.hookenv import log
779
780@@ -29,6 +27,8 @@
781 apt_install('python-pip')
782 from pip import main as pip_execute
783
784+__author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>"
785+
786
787 def parse_options(given, available):
788 """Given a set of options, check if available"""
789
790=== modified file 'hooks/charmhelpers/core/fstab.py'
791--- hooks/charmhelpers/core/fstab.py 2015-01-26 09:45:59 +0000
792+++ hooks/charmhelpers/core/fstab.py 2015-03-06 06:41:14 +0000
793@@ -17,11 +17,11 @@
794 # You should have received a copy of the GNU Lesser General Public License
795 # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
796
797-__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
798-
799 import io
800 import os
801
802+__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
803+
804
805 class Fstab(io.FileIO):
806 """This class extends file in order to implement a file reader/writer
807@@ -77,7 +77,7 @@
808 for line in self.readlines():
809 line = line.decode('us-ascii')
810 try:
811- if line.strip() and not line.startswith("#"):
812+ if line.strip() and not line.strip().startswith("#"):
813 yield self._hydrate_entry(line)
814 except ValueError:
815 pass
816@@ -104,7 +104,7 @@
817
818 found = False
819 for index, line in enumerate(lines):
820- if not line.startswith("#"):
821+ if line.strip() and not line.strip().startswith("#"):
822 if self._hydrate_entry(line) == entry:
823 found = True
824 break
825
826=== modified file 'hooks/charmhelpers/core/host.py'
827--- hooks/charmhelpers/core/host.py 2015-01-26 09:45:59 +0000
828+++ hooks/charmhelpers/core/host.py 2015-03-06 06:41:14 +0000
829@@ -191,11 +191,11 @@
830
831
832 def write_file(path, content, owner='root', group='root', perms=0o444):
833- """Create or overwrite a file with the contents of a string"""
834+ """Create or overwrite a file with the contents of a byte string."""
835 log("Writing file {} {}:{} {:o}".format(path, owner, group, perms))
836 uid = pwd.getpwnam(owner).pw_uid
837 gid = grp.getgrnam(group).gr_gid
838- with open(path, 'w') as target:
839+ with open(path, 'wb') as target:
840 os.fchown(target.fileno(), uid, gid)
841 os.fchmod(target.fileno(), perms)
842 target.write(content)
843@@ -305,11 +305,11 @@
844 ceph_client_changed function.
845 """
846 def wrap(f):
847- def wrapped_f(*args):
848+ def wrapped_f(*args, **kwargs):
849 checksums = {}
850 for path in restart_map:
851 checksums[path] = file_hash(path)
852- f(*args)
853+ f(*args, **kwargs)
854 restarts = []
855 for path in restart_map:
856 if checksums[path] != file_hash(path):
857@@ -361,7 +361,7 @@
858 ip_output = (line for line in ip_output if line)
859 for line in ip_output:
860 if line.split()[1].startswith(int_type):
861- matched = re.search('.*: (bond[0-9]+\.[0-9]+)@.*', line)
862+ matched = re.search('.*: (' + int_type + r'[0-9]+\.[0-9]+)@.*', line)
863 if matched:
864 interface = matched.groups()[0]
865 else:
866
867=== modified file 'hooks/charmhelpers/core/services/helpers.py'
868--- hooks/charmhelpers/core/services/helpers.py 2015-01-26 09:45:59 +0000
869+++ hooks/charmhelpers/core/services/helpers.py 2015-03-06 06:41:14 +0000
870@@ -45,12 +45,14 @@
871 """
872 name = None
873 interface = None
874- required_keys = []
875
876 def __init__(self, name=None, additional_required_keys=None):
877+ if not hasattr(self, 'required_keys'):
878+ self.required_keys = []
879+
880 if name is not None:
881 self.name = name
882- if additional_required_keys is not None:
883+ if additional_required_keys:
884 self.required_keys.extend(additional_required_keys)
885 self.get_data()
886
887@@ -134,7 +136,10 @@
888 """
889 name = 'db'
890 interface = 'mysql'
891- required_keys = ['host', 'user', 'password', 'database']
892+
893+ def __init__(self, *args, **kwargs):
894+ self.required_keys = ['host', 'user', 'password', 'database']
895+ super(HttpRelation).__init__(self, *args, **kwargs)
896
897
898 class HttpRelation(RelationContext):
899@@ -146,7 +151,10 @@
900 """
901 name = 'website'
902 interface = 'http'
903- required_keys = ['host', 'port']
904+
905+ def __init__(self, *args, **kwargs):
906+ self.required_keys = ['host', 'port']
907+ super(HttpRelation).__init__(self, *args, **kwargs)
908
909 def provide_data(self):
910 return {
911
912=== added file 'hooks/charmhelpers/core/strutils.py'
913--- hooks/charmhelpers/core/strutils.py 1970-01-01 00:00:00 +0000
914+++ hooks/charmhelpers/core/strutils.py 2015-03-06 06:41:14 +0000
915@@ -0,0 +1,42 @@
916+#!/usr/bin/env python
917+# -*- coding: utf-8 -*-
918+
919+# Copyright 2014-2015 Canonical Limited.
920+#
921+# This file is part of charm-helpers.
922+#
923+# charm-helpers is free software: you can redistribute it and/or modify
924+# it under the terms of the GNU Lesser General Public License version 3 as
925+# published by the Free Software Foundation.
926+#
927+# charm-helpers is distributed in the hope that it will be useful,
928+# but WITHOUT ANY WARRANTY; without even the implied warranty of
929+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
930+# GNU Lesser General Public License for more details.
931+#
932+# You should have received a copy of the GNU Lesser General Public License
933+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
934+
935+import six
936+
937+
938+def bool_from_string(value):
939+ """Interpret string value as boolean.
940+
941+ Returns True if value translates to True otherwise False.
942+ """
943+ if isinstance(value, six.string_types):
944+ value = six.text_type(value)
945+ else:
946+ msg = "Unable to interpret non-string value '%s' as boolean" % (value)
947+ raise ValueError(msg)
948+
949+ value = value.strip().lower()
950+
951+ if value in ['y', 'yes', 'true', 't']:
952+ return True
953+ elif value in ['n', 'no', 'false', 'f']:
954+ return False
955+
956+ msg = "Unable to interpret string value '%s' as boolean" % (value)
957+ raise ValueError(msg)
958
959=== modified file 'hooks/charmhelpers/core/sysctl.py'
960--- hooks/charmhelpers/core/sysctl.py 2015-01-26 09:45:59 +0000
961+++ hooks/charmhelpers/core/sysctl.py 2015-03-06 06:41:14 +0000
962@@ -17,8 +17,6 @@
963 # You should have received a copy of the GNU Lesser General Public License
964 # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
965
966-__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
967-
968 import yaml
969
970 from subprocess import check_call
971@@ -26,25 +24,33 @@
972 from charmhelpers.core.hookenv import (
973 log,
974 DEBUG,
975+ ERROR,
976 )
977
978+__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
979+
980
981 def create(sysctl_dict, sysctl_file):
982 """Creates a sysctl.conf file from a YAML associative array
983
984- :param sysctl_dict: a dict of sysctl options eg { 'kernel.max_pid': 1337 }
985- :type sysctl_dict: dict
986+ :param sysctl_dict: a YAML-formatted string of sysctl options eg "{ 'kernel.max_pid': 1337 }"
987+ :type sysctl_dict: str
988 :param sysctl_file: path to the sysctl file to be saved
989 :type sysctl_file: str or unicode
990 :returns: None
991 """
992- sysctl_dict = yaml.load(sysctl_dict)
993+ try:
994+ sysctl_dict_parsed = yaml.safe_load(sysctl_dict)
995+ except yaml.YAMLError:
996+ log("Error parsing YAML sysctl_dict: {}".format(sysctl_dict),
997+ level=ERROR)
998+ return
999
1000 with open(sysctl_file, "w") as fd:
1001- for key, value in sysctl_dict.items():
1002+ for key, value in sysctl_dict_parsed.items():
1003 fd.write("{}={}\n".format(key, value))
1004
1005- log("Updating sysctl_file: %s values: %s" % (sysctl_file, sysctl_dict),
1006+ log("Updating sysctl_file: %s values: %s" % (sysctl_file, sysctl_dict_parsed),
1007 level=DEBUG)
1008
1009 check_call(["sysctl", "-p", sysctl_file])
1010
1011=== modified file 'hooks/charmhelpers/core/templating.py'
1012--- hooks/charmhelpers/core/templating.py 2015-01-26 09:45:59 +0000
1013+++ hooks/charmhelpers/core/templating.py 2015-03-06 06:41:14 +0000
1014@@ -21,7 +21,7 @@
1015
1016
1017 def render(source, target, context, owner='root', group='root',
1018- perms=0o444, templates_dir=None):
1019+ perms=0o444, templates_dir=None, encoding='UTF-8'):
1020 """
1021 Render a template.
1022
1023@@ -64,5 +64,5 @@
1024 level=hookenv.ERROR)
1025 raise e
1026 content = template.render(context)
1027- host.mkdir(os.path.dirname(target), owner, group)
1028- host.write_file(target, content, owner, group, perms)
1029+ host.mkdir(os.path.dirname(target), owner, group, perms=0o755)
1030+ host.write_file(target, content.encode(encoding), owner, group, perms)
1031
1032=== added file 'hooks/charmhelpers/core/unitdata.py'
1033--- hooks/charmhelpers/core/unitdata.py 1970-01-01 00:00:00 +0000
1034+++ hooks/charmhelpers/core/unitdata.py 2015-03-06 06:41:14 +0000
1035@@ -0,0 +1,477 @@
1036+#!/usr/bin/env python
1037+# -*- coding: utf-8 -*-
1038+#
1039+# Copyright 2014-2015 Canonical Limited.
1040+#
1041+# This file is part of charm-helpers.
1042+#
1043+# charm-helpers is free software: you can redistribute it and/or modify
1044+# it under the terms of the GNU Lesser General Public License version 3 as
1045+# published by the Free Software Foundation.
1046+#
1047+# charm-helpers is distributed in the hope that it will be useful,
1048+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1049+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1050+# GNU Lesser General Public License for more details.
1051+#
1052+# You should have received a copy of the GNU Lesser General Public License
1053+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1054+#
1055+#
1056+# Authors:
1057+# Kapil Thangavelu <kapil.foss@gmail.com>
1058+#
1059+"""
1060+Intro
1061+-----
1062+
1063+A simple way to store state in units. This provides a key value
1064+storage with support for versioned, transactional operation,
1065+and can calculate deltas from previous values to simplify unit logic
1066+when processing changes.
1067+
1068+
1069+Hook Integration
1070+----------------
1071+
1072+There are several extant frameworks for hook execution, including
1073+
1074+ - charmhelpers.core.hookenv.Hooks
1075+ - charmhelpers.core.services.ServiceManager
1076+
1077+The storage classes are framework agnostic, one simple integration is
1078+via the HookData contextmanager. It will record the current hook
1079+execution environment (including relation data, config data, etc.),
1080+setup a transaction and allow easy access to the changes from
1081+previously seen values. One consequence of the integration is the
1082+reservation of particular keys ('rels', 'unit', 'env', 'config',
1083+'charm_revisions') for their respective values.
1084+
1085+Here's a fully worked integration example using hookenv.Hooks::
1086+
1087+ from charmhelper.core import hookenv, unitdata
1088+
1089+ hook_data = unitdata.HookData()
1090+ db = unitdata.kv()
1091+ hooks = hookenv.Hooks()
1092+
1093+ @hooks.hook
1094+ def config_changed():
1095+ # Print all changes to configuration from previously seen
1096+ # values.
1097+ for changed, (prev, cur) in hook_data.conf.items():
1098+ print('config changed', changed,
1099+ 'previous value', prev,
1100+ 'current value', cur)
1101+
1102+ # Get some unit specific bookeeping
1103+ if not db.get('pkg_key'):
1104+ key = urllib.urlopen('https://example.com/pkg_key').read()
1105+ db.set('pkg_key', key)
1106+
1107+ # Directly access all charm config as a mapping.
1108+ conf = db.getrange('config', True)
1109+
1110+ # Directly access all relation data as a mapping
1111+ rels = db.getrange('rels', True)
1112+
1113+ if __name__ == '__main__':
1114+ with hook_data():
1115+ hook.execute()
1116+
1117+
1118+A more basic integration is via the hook_scope context manager which simply
1119+manages transaction scope (and records hook name, and timestamp)::
1120+
1121+ >>> from unitdata import kv
1122+ >>> db = kv()
1123+ >>> with db.hook_scope('install'):
1124+ ... # do work, in transactional scope.
1125+ ... db.set('x', 1)
1126+ >>> db.get('x')
1127+ 1
1128+
1129+
1130+Usage
1131+-----
1132+
1133+Values are automatically json de/serialized to preserve basic typing
1134+and complex data struct capabilities (dicts, lists, ints, booleans, etc).
1135+
1136+Individual values can be manipulated via get/set::
1137+
1138+ >>> kv.set('y', True)
1139+ >>> kv.get('y')
1140+ True
1141+
1142+ # We can set complex values (dicts, lists) as a single key.
1143+ >>> kv.set('config', {'a': 1, 'b': True'})
1144+
1145+ # Also supports returning dictionaries as a record which
1146+ # provides attribute access.
1147+ >>> config = kv.get('config', record=True)
1148+ >>> config.b
1149+ True
1150+
1151+
1152+Groups of keys can be manipulated with update/getrange::
1153+
1154+ >>> kv.update({'z': 1, 'y': 2}, prefix="gui.")
1155+ >>> kv.getrange('gui.', strip=True)
1156+ {'z': 1, 'y': 2}
1157+
1158+When updating values, its very helpful to understand which values
1159+have actually changed and how have they changed. The storage
1160+provides a delta method to provide for this::
1161+
1162+ >>> data = {'debug': True, 'option': 2}
1163+ >>> delta = kv.delta(data, 'config.')
1164+ >>> delta.debug.previous
1165+ None
1166+ >>> delta.debug.current
1167+ True
1168+ >>> delta
1169+ {'debug': (None, True), 'option': (None, 2)}
1170+
1171+Note the delta method does not persist the actual change, it needs to
1172+be explicitly saved via 'update' method::
1173+
1174+ >>> kv.update(data, 'config.')
1175+
1176+Values modified in the context of a hook scope retain historical values
1177+associated to the hookname.
1178+
1179+ >>> with db.hook_scope('config-changed'):
1180+ ... db.set('x', 42)
1181+ >>> db.gethistory('x')
1182+ [(1, u'x', 1, u'install', u'2015-01-21T16:49:30.038372'),
1183+ (2, u'x', 42, u'config-changed', u'2015-01-21T16:49:30.038786')]
1184+
1185+"""
1186+
1187+import collections
1188+import contextlib
1189+import datetime
1190+import json
1191+import os
1192+import pprint
1193+import sqlite3
1194+import sys
1195+
1196+__author__ = 'Kapil Thangavelu <kapil.foss@gmail.com>'
1197+
1198+
1199+class Storage(object):
1200+ """Simple key value database for local unit state within charms.
1201+
1202+ Modifications are automatically committed at hook exit. That's
1203+ currently regardless of exit code.
1204+
1205+ To support dicts, lists, integer, floats, and booleans values
1206+ are automatically json encoded/decoded.
1207+ """
1208+ def __init__(self, path=None):
1209+ self.db_path = path
1210+ if path is None:
1211+ self.db_path = os.path.join(
1212+ os.environ.get('CHARM_DIR', ''), '.unit-state.db')
1213+ self.conn = sqlite3.connect('%s' % self.db_path)
1214+ self.cursor = self.conn.cursor()
1215+ self.revision = None
1216+ self._closed = False
1217+ self._init()
1218+
1219+ def close(self):
1220+ if self._closed:
1221+ return
1222+ self.flush(False)
1223+ self.cursor.close()
1224+ self.conn.close()
1225+ self._closed = True
1226+
1227+ def _scoped_query(self, stmt, params=None):
1228+ if params is None:
1229+ params = []
1230+ return stmt, params
1231+
1232+ def get(self, key, default=None, record=False):
1233+ self.cursor.execute(
1234+ *self._scoped_query(
1235+ 'select data from kv where key=?', [key]))
1236+ result = self.cursor.fetchone()
1237+ if not result:
1238+ return default
1239+ if record:
1240+ return Record(json.loads(result[0]))
1241+ return json.loads(result[0])
1242+
1243+ def getrange(self, key_prefix, strip=False):
1244+ stmt = "select key, data from kv where key like '%s%%'" % key_prefix
1245+ self.cursor.execute(*self._scoped_query(stmt))
1246+ result = self.cursor.fetchall()
1247+
1248+ if not result:
1249+ return None
1250+ if not strip:
1251+ key_prefix = ''
1252+ return dict([
1253+ (k[len(key_prefix):], json.loads(v)) for k, v in result])
1254+
1255+ def update(self, mapping, prefix=""):
1256+ for k, v in mapping.items():
1257+ self.set("%s%s" % (prefix, k), v)
1258+
1259+ def unset(self, key):
1260+ self.cursor.execute('delete from kv where key=?', [key])
1261+ if self.revision and self.cursor.rowcount:
1262+ self.cursor.execute(
1263+ 'insert into kv_revisions values (?, ?, ?)',
1264+ [key, self.revision, json.dumps('DELETED')])
1265+
1266+ def set(self, key, value):
1267+ serialized = json.dumps(value)
1268+
1269+ self.cursor.execute(
1270+ 'select data from kv where key=?', [key])
1271+ exists = self.cursor.fetchone()
1272+
1273+ # Skip mutations to the same value
1274+ if exists:
1275+ if exists[0] == serialized:
1276+ return value
1277+
1278+ if not exists:
1279+ self.cursor.execute(
1280+ 'insert into kv (key, data) values (?, ?)',
1281+ (key, serialized))
1282+ else:
1283+ self.cursor.execute('''
1284+ update kv
1285+ set data = ?
1286+ where key = ?''', [serialized, key])
1287+
1288+ # Save
1289+ if not self.revision:
1290+ return value
1291+
1292+ self.cursor.execute(
1293+ 'select 1 from kv_revisions where key=? and revision=?',
1294+ [key, self.revision])
1295+ exists = self.cursor.fetchone()
1296+
1297+ if not exists:
1298+ self.cursor.execute(
1299+ '''insert into kv_revisions (
1300+ revision, key, data) values (?, ?, ?)''',
1301+ (self.revision, key, serialized))
1302+ else:
1303+ self.cursor.execute(
1304+ '''
1305+ update kv_revisions
1306+ set data = ?
1307+ where key = ?
1308+ and revision = ?''',
1309+ [serialized, key, self.revision])
1310+
1311+ return value
1312+
1313+ def delta(self, mapping, prefix):
1314+ """
1315+ return a delta containing values that have changed.
1316+ """
1317+ previous = self.getrange(prefix, strip=True)
1318+ if not previous:
1319+ pk = set()
1320+ else:
1321+ pk = set(previous.keys())
1322+ ck = set(mapping.keys())
1323+ delta = DeltaSet()
1324+
1325+ # added
1326+ for k in ck.difference(pk):
1327+ delta[k] = Delta(None, mapping[k])
1328+
1329+ # removed
1330+ for k in pk.difference(ck):
1331+ delta[k] = Delta(previous[k], None)
1332+
1333+ # changed
1334+ for k in pk.intersection(ck):
1335+ c = mapping[k]
1336+ p = previous[k]
1337+ if c != p:
1338+ delta[k] = Delta(p, c)
1339+
1340+ return delta
1341+
1342+ @contextlib.contextmanager
1343+ def hook_scope(self, name=""):
1344+ """Scope all future interactions to the current hook execution
1345+ revision."""
1346+ assert not self.revision
1347+ self.cursor.execute(
1348+ 'insert into hooks (hook, date) values (?, ?)',
1349+ (name or sys.argv[0],
1350+ datetime.datetime.utcnow().isoformat()))
1351+ self.revision = self.cursor.lastrowid
1352+ try:
1353+ yield self.revision
1354+ self.revision = None
1355+ except:
1356+ self.flush(False)
1357+ self.revision = None
1358+ raise
1359+ else:
1360+ self.flush()
1361+
1362+ def flush(self, save=True):
1363+ if save:
1364+ self.conn.commit()
1365+ elif self._closed:
1366+ return
1367+ else:
1368+ self.conn.rollback()
1369+
1370+ def _init(self):
1371+ self.cursor.execute('''
1372+ create table if not exists kv (
1373+ key text,
1374+ data text,
1375+ primary key (key)
1376+ )''')
1377+ self.cursor.execute('''
1378+ create table if not exists kv_revisions (
1379+ key text,
1380+ revision integer,
1381+ data text,
1382+ primary key (key, revision)
1383+ )''')
1384+ self.cursor.execute('''
1385+ create table if not exists hooks (
1386+ version integer primary key autoincrement,
1387+ hook text,
1388+ date text
1389+ )''')
1390+ self.conn.commit()
1391+
1392+ def gethistory(self, key, deserialize=False):
1393+ self.cursor.execute(
1394+ '''
1395+ select kv.revision, kv.key, kv.data, h.hook, h.date
1396+ from kv_revisions kv,
1397+ hooks h
1398+ where kv.key=?
1399+ and kv.revision = h.version
1400+ ''', [key])
1401+ if deserialize is False:
1402+ return self.cursor.fetchall()
1403+ return map(_parse_history, self.cursor.fetchall())
1404+
1405+ def debug(self, fh=sys.stderr):
1406+ self.cursor.execute('select * from kv')
1407+ pprint.pprint(self.cursor.fetchall(), stream=fh)
1408+ self.cursor.execute('select * from kv_revisions')
1409+ pprint.pprint(self.cursor.fetchall(), stream=fh)
1410+
1411+
1412+def _parse_history(d):
1413+ return (d[0], d[1], json.loads(d[2]), d[3],
1414+ datetime.datetime.strptime(d[-1], "%Y-%m-%dT%H:%M:%S.%f"))
1415+
1416+
1417+class HookData(object):
1418+ """Simple integration for existing hook exec frameworks.
1419+
1420+ Records all unit information, and stores deltas for processing
1421+ by the hook.
1422+
1423+ Sample::
1424+
1425+ from charmhelper.core import hookenv, unitdata
1426+
1427+ changes = unitdata.HookData()
1428+ db = unitdata.kv()
1429+ hooks = hookenv.Hooks()
1430+
1431+ @hooks.hook
1432+ def config_changed():
1433+ # View all changes to configuration
1434+ for changed, (prev, cur) in changes.conf.items():
1435+ print('config changed', changed,
1436+ 'previous value', prev,
1437+ 'current value', cur)
1438+
1439+ # Get some unit specific bookeeping
1440+ if not db.get('pkg_key'):
1441+ key = urllib.urlopen('https://example.com/pkg_key').read()
1442+ db.set('pkg_key', key)
1443+
1444+ if __name__ == '__main__':
1445+ with changes():
1446+ hook.execute()
1447+
1448+ """
1449+ def __init__(self):
1450+ self.kv = kv()
1451+ self.conf = None
1452+ self.rels = None
1453+
1454+ @contextlib.contextmanager
1455+ def __call__(self):
1456+ from charmhelpers.core import hookenv
1457+ hook_name = hookenv.hook_name()
1458+
1459+ with self.kv.hook_scope(hook_name):
1460+ self._record_charm_version(hookenv.charm_dir())
1461+ delta_config, delta_relation = self._record_hook(hookenv)
1462+ yield self.kv, delta_config, delta_relation
1463+
1464+ def _record_charm_version(self, charm_dir):
1465+ # Record revisions.. charm revisions are meaningless
1466+ # to charm authors as they don't control the revision.
1467+ # so logic dependnent on revision is not particularly
1468+ # useful, however it is useful for debugging analysis.
1469+ charm_rev = open(
1470+ os.path.join(charm_dir, 'revision')).read().strip()
1471+ charm_rev = charm_rev or '0'
1472+ revs = self.kv.get('charm_revisions', [])
1473+ if charm_rev not in revs:
1474+ revs.append(charm_rev.strip() or '0')
1475+ self.kv.set('charm_revisions', revs)
1476+
1477+ def _record_hook(self, hookenv):
1478+ data = hookenv.execution_environment()
1479+ self.conf = conf_delta = self.kv.delta(data['conf'], 'config')
1480+ self.rels = rels_delta = self.kv.delta(data['rels'], 'rels')
1481+ self.kv.set('env', data['env'])
1482+ self.kv.set('unit', data['unit'])
1483+ self.kv.set('relid', data.get('relid'))
1484+ return conf_delta, rels_delta
1485+
1486+
1487+class Record(dict):
1488+
1489+ __slots__ = ()
1490+
1491+ def __getattr__(self, k):
1492+ if k in self:
1493+ return self[k]
1494+ raise AttributeError(k)
1495+
1496+
1497+class DeltaSet(Record):
1498+
1499+ __slots__ = ()
1500+
1501+
1502+Delta = collections.namedtuple('Delta', ['previous', 'current'])
1503+
1504+
1505+_KV = None
1506+
1507+
1508+def kv():
1509+ global _KV
1510+ if _KV is None:
1511+ _KV = Storage()
1512+ return _KV
1513
1514=== modified file 'hooks/charmhelpers/fetch/archiveurl.py'
1515--- hooks/charmhelpers/fetch/archiveurl.py 2015-01-26 09:45:59 +0000
1516+++ hooks/charmhelpers/fetch/archiveurl.py 2015-03-06 06:41:14 +0000
1517@@ -18,6 +18,16 @@
1518 import hashlib
1519 import re
1520
1521+from charmhelpers.fetch import (
1522+ BaseFetchHandler,
1523+ UnhandledSource
1524+)
1525+from charmhelpers.payload.archive import (
1526+ get_archive_handler,
1527+ extract,
1528+)
1529+from charmhelpers.core.host import mkdir, check_hash
1530+
1531 import six
1532 if six.PY3:
1533 from urllib.request import (
1534@@ -35,16 +45,6 @@
1535 )
1536 from urlparse import urlparse, urlunparse, parse_qs
1537
1538-from charmhelpers.fetch import (
1539- BaseFetchHandler,
1540- UnhandledSource
1541-)
1542-from charmhelpers.payload.archive import (
1543- get_archive_handler,
1544- extract,
1545-)
1546-from charmhelpers.core.host import mkdir, check_hash
1547-
1548
1549 def splituser(host):
1550 '''urllib.splituser(), but six's support of this seems broken'''
1551
1552=== modified file 'hooks/charmhelpers/fetch/giturl.py'
1553--- hooks/charmhelpers/fetch/giturl.py 2015-01-26 09:45:59 +0000
1554+++ hooks/charmhelpers/fetch/giturl.py 2015-03-06 06:41:14 +0000
1555@@ -32,7 +32,7 @@
1556 apt_install("python-git")
1557 from git import Repo
1558
1559-from git.exc import GitCommandError
1560+from git.exc import GitCommandError # noqa E402
1561
1562
1563 class GitUrlFetchHandler(BaseFetchHandler):
1564
1565=== modified file 'hooks/rabbit_utils.py'
1566--- hooks/rabbit_utils.py 2015-01-22 15:37:28 +0000
1567+++ hooks/rabbit_utils.py 2015-03-06 06:41:14 +0000
1568@@ -570,3 +570,11 @@
1569 if svcs:
1570 _map.append((f, svcs))
1571 return OrderedDict(_map)
1572+
1573+
1574+def services():
1575+ ''' Returns a list of services associate with this charm '''
1576+ _services = []
1577+ for v in restart_map().values():
1578+ _services = _services + v
1579+ return list(set(_services))
1580
1581=== modified file 'hooks/rabbitmq_server_relations.py'
1582--- hooks/rabbitmq_server_relations.py 2015-01-27 08:43:50 +0000
1583+++ hooks/rabbitmq_server_relations.py 2015-03-06 06:41:14 +0000
1584@@ -60,7 +60,7 @@
1585 service_stop,
1586 service_restart,
1587 )
1588-from charmhelpers.contrib.charmsupport.nrpe import NRPE
1589+from charmhelpers.contrib.charmsupport import nrpe
1590 from charmhelpers.contrib.ssl.service import ServiceCA
1591
1592 from charmhelpers.contrib.peerstorage import (
1593@@ -474,29 +474,20 @@
1594 os.path.join(NAGIOS_PLUGINS, 'check_rabbitmq.py'))
1595
1596 # Find out if nrpe set nagios_hostname
1597- hostname = None
1598- host_context = None
1599- for rel in relations_of_type('nrpe-external-master'):
1600- if 'nagios_hostname' in rel:
1601- hostname = rel['nagios_hostname']
1602- host_context = rel['nagios_host_context']
1603- break
1604+ hostname = nrpe.get_nagios_hostname()
1605+ myunit = nrpe.get_nagios_unit_name()
1606+
1607 # create unique user and vhost for each unit
1608 current_unit = local_unit().replace('/', '-')
1609 user = 'nagios-%s' % current_unit
1610 vhost = 'nagios-%s' % current_unit
1611 password = rabbit.get_rabbit_password(user)
1612
1613- if host_context:
1614- myunit = "%s:%s" % (host_context, local_unit())
1615- else:
1616- myunit = local_unit()
1617-
1618 rabbit.create_vhost(vhost)
1619 rabbit.create_user(user, password)
1620 rabbit.grant_permissions(user, vhost)
1621
1622- nrpe_compat = NRPE(hostname=hostname)
1623+ nrpe_compat = nrpe.NRPE(hostname=hostname)
1624 nrpe_compat.add_check(
1625 shortname=rabbit.RABBIT_USER,
1626 description='Check RabbitMQ {%s}' % myunit,

Subscribers

People subscribed via source and target branches