Merge lp:~bloodearnest/charms/trusty/rabbitmq-server/add-nagios-service-groups into lp:charms/trusty/rabbitmq-server

Proposed by Simon Davy on 2015-03-04
Status: Work in progress
Proposed branch: lp:~bloodearnest/charms/trusty/rabbitmq-server/add-nagios-service-groups
Merge into: lp:charms/trusty/rabbitmq-server
Diff against target: 1394 lines (+836/-131)
21 files modified
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 (+62/-10)
hooks/charmhelpers/contrib/openstack/files/__init__.py (+18/-0)
hooks/charmhelpers/contrib/openstack/ip.py (+37/-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)
unit_tests/test_rabbit_utils.py (+1/-1)
unit_tests/test_rabbitmq_server_relations.py (+1/-1)
To merge this branch: bzr merge lp:~bloodearnest/charms/trusty/rabbitmq-server/add-nagios-service-groups
Reviewer Review Type Date Requested Status
Marco Ceppi 2015-03-04 Needs Fixing on 2015-03-24
Cory Johns Approve on 2015-03-24
Review via email: mp+251790@code.launchpad.net

Commit Message

Add nagios_servicegroups config, and update to latest charmhelpers to get sane default behaviour for when nagios_servicegroups is not set.

Description of the Change

Add nagios_servicegroups config, and update to latest charmhelpers to get sane default behaviour for when nagios_servicegroups is not set.

To post a comment you must log in.
84. By Simon Davy on 2015-03-04

typo

charm_unit_test #2256 rabbitmq-server for bloodearnest mp251790
    UNIT OK: passed

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

charm_lint_check #2466 rabbitmq-server for bloodearnest mp251790
    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/10529446/
Build: http://10.245.162.77:8080/job/charm_lint_check/2466/

charm_amulet_test #2342 rabbitmq-server for bloodearnest mp251790
    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/10529503/
Build: http://10.245.162.77:8080/job/charm_amulet_test/2342/

85. By Simon Davy on 2015-03-05

fix lint issues

charm_unit_test #2264 rabbitmq-server for bloodearnest mp251790
    UNIT OK: passed

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

charm_lint_check #2474 rabbitmq-server for bloodearnest mp251790
    LINT OK: passed

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

charm_amulet_test #2347 rabbitmq-server for bloodearnest mp251790
    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/10537970/
Build: http://10.245.162.77:8080/job/charm_amulet_test/2347/

Cory Johns (johnsca) wrote :

Simon,

The CI failure appears to be due to a cert issue in the upstream branch, which has been reported in this bug: https://bugs.launchpad.net/charms/+source/rabbitmq-server/+bug/1436014

The changes in this MP seem reasonable, and setting and unsetting the new config option seems to work without error (although I am not entirely clear how to verify the expected result), so I'm willing to give this MP my +1. However, getting the test failure fixed upstream will very much make these reviews cleaner going forward.

review: Approve
Marco Ceppi (marcoceppi) wrote :

This conflicts with the current upstream, can you merge and resolve?

review: Needs Fixing

Unmerged revisions

85. By Simon Davy on 2015-03-05

fix lint issues

84. By Simon Davy on 2015-03-04

typo

83. By Simon Davy on 2015-03-04

Add nagios_servicegroups config, and update to latest charmhelpers to get sane default behaviour for when nagios_servicegroups is not set

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

Subscribers

People subscribed via source and target branches