Merge lp:~1chb1n/charms/trusty/nova-cell/next-amulet-debug-and-makefile into lp:~openstack-charmers/charms/trusty/nova-cell/next

Proposed by Ryan Beisner
Status: Merged
Merged at revision: 65
Proposed branch: lp:~1chb1n/charms/trusty/nova-cell/next-amulet-debug-and-makefile
Merge into: lp:~openstack-charmers/charms/trusty/nova-cell/next
Diff against target: 2271 lines (+1460/-208)
20 files modified
hooks/charmhelpers/contrib/network/ip.py (+84/-1)
hooks/charmhelpers/contrib/openstack/amulet/deployment.py (+34/-5)
hooks/charmhelpers/contrib/openstack/context.py (+289/-15)
hooks/charmhelpers/contrib/openstack/files/__init__.py (+18/-0)
hooks/charmhelpers/contrib/openstack/ip.py (+37/-0)
hooks/charmhelpers/contrib/openstack/neutron.py (+83/-0)
hooks/charmhelpers/contrib/openstack/utils.py (+142/-141)
hooks/charmhelpers/contrib/python/packages.py (+2/-2)
hooks/charmhelpers/core/fstab.py (+4/-4)
hooks/charmhelpers/core/hookenv.py (+40/-1)
hooks/charmhelpers/core/host.py (+10/-6)
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)
tests/charmhelpers/contrib/amulet/utils.py (+125/-3)
tests/charmhelpers/contrib/openstack/amulet/deployment.py (+34/-5)
To merge this branch: bzr merge lp:~1chb1n/charms/trusty/nova-cell/next-amulet-debug-and-makefile
Reviewer Review Type Date Requested Status
OpenStack Charmers Pending
Review via email: mp+256591@code.launchpad.net

Description of the change

auto sync charmhelpers

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

charm_lint_check #3526 nova-cell-next for 1chb1n mp256591
    LINT OK: passed

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

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

charm_unit_test #3314 nova-cell-next for 1chb1n mp256591
    UNIT OK: passed

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

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

charm_amulet_test #3281 nova-cell-next for 1chb1n mp256591
    AMULET FAIL: amulet-test failed

AMULET Results (max last 2 lines):
make: *** [test] Error 1
ERROR:root:Make target returned non-zero.

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

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

charm_lint_check #3550 nova-cell-next for 1chb1n mp256591
    LINT OK: passed

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

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

charm_lint_check #3560 nova-cell-next for 1chb1n mp256591
    LINT OK: passed

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

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

charm_unit_test #3348 nova-cell-next for 1chb1n mp256591
    UNIT OK: passed

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

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

charm_amulet_test #3317 nova-cell-next for 1chb1n mp256591
    AMULET FAIL: amulet-test failed

AMULET Results (max last 2 lines):
make: *** [test] Error 1
ERROR:root:Make target returned non-zero.

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

Revision history for this message
Ryan Beisner (1chb1n) wrote :

The amulet fail is actually due to missing / no tests in this charm.

00:01:08.349 juju-test CRITICAL: No tests were found

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'hooks/charmhelpers/contrib/network/ip.py'
2--- hooks/charmhelpers/contrib/network/ip.py 2015-01-29 13:02:55 +0000
3+++ hooks/charmhelpers/contrib/network/ip.py 2015-04-16 21:56:47 +0000
4@@ -17,13 +17,16 @@
5 import glob
6 import re
7 import subprocess
8+import six
9+import socket
10
11 from functools import partial
12
13 from charmhelpers.core.hookenv import unit_get
14 from charmhelpers.fetch import apt_install
15 from charmhelpers.core.hookenv import (
16- log
17+ log,
18+ WARNING,
19 )
20
21 try:
22@@ -365,3 +368,83 @@
23 return True
24
25 return False
26+
27+
28+def is_ip(address):
29+ """
30+ Returns True if address is a valid IP address.
31+ """
32+ try:
33+ # Test to see if already an IPv4 address
34+ socket.inet_aton(address)
35+ return True
36+ except socket.error:
37+ return False
38+
39+
40+def ns_query(address):
41+ try:
42+ import dns.resolver
43+ except ImportError:
44+ apt_install('python-dnspython')
45+ import dns.resolver
46+
47+ if isinstance(address, dns.name.Name):
48+ rtype = 'PTR'
49+ elif isinstance(address, six.string_types):
50+ rtype = 'A'
51+ else:
52+ return None
53+
54+ answers = dns.resolver.query(address, rtype)
55+ if answers:
56+ return str(answers[0])
57+ return None
58+
59+
60+def get_host_ip(hostname, fallback=None):
61+ """
62+ Resolves the IP for a given hostname, or returns
63+ the input if it is already an IP.
64+ """
65+ if is_ip(hostname):
66+ return hostname
67+
68+ ip_addr = ns_query(hostname)
69+ if not ip_addr:
70+ try:
71+ ip_addr = socket.gethostbyname(hostname)
72+ except:
73+ log("Failed to resolve hostname '%s'" % (hostname),
74+ level=WARNING)
75+ return fallback
76+ return ip_addr
77+
78+
79+def get_hostname(address, fqdn=True):
80+ """
81+ Resolves hostname for given IP, or returns the input
82+ if it is already a hostname.
83+ """
84+ if is_ip(address):
85+ try:
86+ import dns.reversename
87+ except ImportError:
88+ apt_install("python-dnspython")
89+ import dns.reversename
90+
91+ rev = dns.reversename.from_address(address)
92+ result = ns_query(rev)
93+ if not result:
94+ return None
95+ else:
96+ result = address
97+
98+ if fqdn:
99+ # strip trailing .
100+ if result.endswith('.'):
101+ return result[:-1]
102+ else:
103+ return result
104+ else:
105+ return result.split('.')[0]
106
107=== modified file 'hooks/charmhelpers/contrib/openstack/amulet/deployment.py'
108--- hooks/charmhelpers/contrib/openstack/amulet/deployment.py 2015-01-29 13:02:55 +0000
109+++ hooks/charmhelpers/contrib/openstack/amulet/deployment.py 2015-04-16 21:56:47 +0000
110@@ -15,6 +15,7 @@
111 # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
112
113 import six
114+from collections import OrderedDict
115 from charmhelpers.contrib.amulet.deployment import (
116 AmuletDeployment
117 )
118@@ -43,7 +44,7 @@
119 Determine if the local branch being tested is derived from its
120 stable or next (dev) branch, and based on this, use the corresonding
121 stable or next branches for the other_services."""
122- base_charms = ['mysql', 'mongodb', 'rabbitmq-server']
123+ base_charms = ['mysql', 'mongodb']
124
125 if self.stable:
126 for svc in other_services:
127@@ -71,16 +72,19 @@
128 services.append(this_service)
129 use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph',
130 'ceph-osd', 'ceph-radosgw']
131+ # Openstack subordinate charms do not expose an origin option as that
132+ # is controlled by the principle
133+ ignore = ['neutron-openvswitch']
134
135 if self.openstack:
136 for svc in services:
137- if svc['name'] not in use_source:
138+ if svc['name'] not in use_source + ignore:
139 config = {'openstack-origin': self.openstack}
140 self.d.configure(svc['name'], config)
141
142 if self.source:
143 for svc in services:
144- if svc['name'] in use_source:
145+ if svc['name'] in use_source and svc['name'] not in ignore:
146 config = {'source': self.source}
147 self.d.configure(svc['name'], config)
148
149@@ -97,12 +101,37 @@
150 """
151 (self.precise_essex, self.precise_folsom, self.precise_grizzly,
152 self.precise_havana, self.precise_icehouse,
153- self.trusty_icehouse) = range(6)
154+ self.trusty_icehouse, self.trusty_juno, self.trusty_kilo,
155+ self.utopic_juno, self.vivid_kilo) = range(10)
156 releases = {
157 ('precise', None): self.precise_essex,
158 ('precise', 'cloud:precise-folsom'): self.precise_folsom,
159 ('precise', 'cloud:precise-grizzly'): self.precise_grizzly,
160 ('precise', 'cloud:precise-havana'): self.precise_havana,
161 ('precise', 'cloud:precise-icehouse'): self.precise_icehouse,
162- ('trusty', None): self.trusty_icehouse}
163+ ('trusty', None): self.trusty_icehouse,
164+ ('trusty', 'cloud:trusty-juno'): self.trusty_juno,
165+ ('trusty', 'cloud:trusty-kilo'): self.trusty_kilo,
166+ ('utopic', None): self.utopic_juno,
167+ ('vivid', None): self.vivid_kilo}
168 return releases[(self.series, self.openstack)]
169+
170+ def _get_openstack_release_string(self):
171+ """Get openstack release string.
172+
173+ Return a string representing the openstack release.
174+ """
175+ releases = OrderedDict([
176+ ('precise', 'essex'),
177+ ('quantal', 'folsom'),
178+ ('raring', 'grizzly'),
179+ ('saucy', 'havana'),
180+ ('trusty', 'icehouse'),
181+ ('utopic', 'juno'),
182+ ('vivid', 'kilo'),
183+ ])
184+ if self.openstack:
185+ os_origin = self.openstack.split(':')[1]
186+ return os_origin.split('%s-' % self.series)[1].split('/')[0]
187+ else:
188+ return releases[self.series]
189
190=== modified file 'hooks/charmhelpers/contrib/openstack/context.py'
191--- hooks/charmhelpers/contrib/openstack/context.py 2015-01-29 13:02:55 +0000
192+++ hooks/charmhelpers/contrib/openstack/context.py 2015-04-16 21:56:47 +0000
193@@ -16,11 +16,13 @@
194
195 import json
196 import os
197+import re
198 import time
199 from base64 import b64decode
200 from subprocess import check_call
201
202 import six
203+import yaml
204
205 from charmhelpers.fetch import (
206 apt_install,
207@@ -45,8 +47,11 @@
208 )
209
210 from charmhelpers.core.sysctl import create as sysctl_create
211+from charmhelpers.core.strutils import bool_from_string
212
213 from charmhelpers.core.host import (
214+ list_nics,
215+ get_nic_hwaddr,
216 mkdir,
217 write_file,
218 )
219@@ -63,16 +68,22 @@
220 )
221 from charmhelpers.contrib.openstack.neutron import (
222 neutron_plugin_attribute,
223+ parse_data_port_mappings,
224+)
225+from charmhelpers.contrib.openstack.ip import (
226+ resolve_address,
227+ INTERNAL,
228 )
229 from charmhelpers.contrib.network.ip import (
230 get_address_in_network,
231+ get_ipv4_addr,
232 get_ipv6_addr,
233 get_netmask_for_address,
234 format_ipv6_addr,
235 is_address_in_network,
236+ is_bridge_member,
237 )
238 from charmhelpers.contrib.openstack.utils import get_host_ip
239-
240 CA_CERT_PATH = '/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt'
241 ADDRESS_TYPES = ['admin', 'internal', 'public']
242
243@@ -104,9 +115,41 @@
244 def config_flags_parser(config_flags):
245 """Parses config flags string into dict.
246
247+ This parsing method supports a few different formats for the config
248+ flag values to be parsed:
249+
250+ 1. A string in the simple format of key=value pairs, with the possibility
251+ of specifying multiple key value pairs within the same string. For
252+ example, a string in the format of 'key1=value1, key2=value2' will
253+ return a dict of:
254+ {'key1': 'value1',
255+ 'key2': 'value2'}.
256+
257+ 2. A string in the above format, but supporting a comma-delimited list
258+ of values for the same key. For example, a string in the format of
259+ 'key1=value1, key2=value3,value4,value5' will return a dict of:
260+ {'key1', 'value1',
261+ 'key2', 'value2,value3,value4'}
262+
263+ 3. A string containing a colon character (:) prior to an equal
264+ character (=) will be treated as yaml and parsed as such. This can be
265+ used to specify more complex key value pairs. For example,
266+ a string in the format of 'key1: subkey1=value1, subkey2=value2' will
267+ return a dict of:
268+ {'key1', 'subkey1=value1, subkey2=value2'}
269+
270 The provided config_flags string may be a list of comma-separated values
271 which themselves may be comma-separated list of values.
272 """
273+ # If we find a colon before an equals sign then treat it as yaml.
274+ # Note: limit it to finding the colon first since this indicates assignment
275+ # for inline yaml.
276+ colon = config_flags.find(':')
277+ equals = config_flags.find('=')
278+ if colon > 0:
279+ if colon < equals or equals < 0:
280+ return yaml.safe_load(config_flags)
281+
282 if config_flags.find('==') >= 0:
283 log("config_flags is not in expected format (key=value)", level=ERROR)
284 raise OSContextError
285@@ -191,7 +234,7 @@
286 unit=local_unit())
287 if set_hostname != access_hostname:
288 relation_set(relation_settings={hostname_key: access_hostname})
289- return ctxt # Defer any further hook execution for now....
290+ return None # Defer any further hook execution for now....
291
292 password_setting = 'password'
293 if self.relation_prefix:
294@@ -277,12 +320,29 @@
295
296
297 class IdentityServiceContext(OSContextGenerator):
298- interfaces = ['identity-service']
299+
300+ def __init__(self, service=None, service_user=None, rel_name='identity-service'):
301+ self.service = service
302+ self.service_user = service_user
303+ self.rel_name = rel_name
304+ self.interfaces = [self.rel_name]
305
306 def __call__(self):
307- log('Generating template context for identity-service', level=DEBUG)
308+ log('Generating template context for ' + self.rel_name, level=DEBUG)
309 ctxt = {}
310- for rid in relation_ids('identity-service'):
311+
312+ if self.service and self.service_user:
313+ # This is required for pki token signing if we don't want /tmp to
314+ # be used.
315+ cachedir = '/var/cache/%s' % (self.service)
316+ if not os.path.isdir(cachedir):
317+ log("Creating service cache dir %s" % (cachedir), level=DEBUG)
318+ mkdir(path=cachedir, owner=self.service_user,
319+ group=self.service_user, perms=0o700)
320+
321+ ctxt['signing_dir'] = cachedir
322+
323+ for rid in relation_ids(self.rel_name):
324 for unit in related_units(rid):
325 rdata = relation_get(rid=rid, unit=unit)
326 serv_host = rdata.get('service_host')
327@@ -291,15 +351,16 @@
328 auth_host = format_ipv6_addr(auth_host) or auth_host
329 svc_protocol = rdata.get('service_protocol') or 'http'
330 auth_protocol = rdata.get('auth_protocol') or 'http'
331- ctxt = {'service_port': rdata.get('service_port'),
332- 'service_host': serv_host,
333- 'auth_host': auth_host,
334- 'auth_port': rdata.get('auth_port'),
335- 'admin_tenant_name': rdata.get('service_tenant'),
336- 'admin_user': rdata.get('service_username'),
337- 'admin_password': rdata.get('service_password'),
338- 'service_protocol': svc_protocol,
339- 'auth_protocol': auth_protocol}
340+ ctxt.update({'service_port': rdata.get('service_port'),
341+ 'service_host': serv_host,
342+ 'auth_host': auth_host,
343+ 'auth_port': rdata.get('auth_port'),
344+ 'admin_tenant_name': rdata.get('service_tenant'),
345+ 'admin_user': rdata.get('service_username'),
346+ 'admin_password': rdata.get('service_password'),
347+ 'service_protocol': svc_protocol,
348+ 'auth_protocol': auth_protocol})
349+
350 if context_complete(ctxt):
351 # NOTE(jamespage) this is required for >= icehouse
352 # so a missing value just indicates keystone needs
353@@ -398,6 +459,11 @@
354
355 ctxt['rabbitmq_hosts'] = ','.join(sorted(rabbitmq_hosts))
356
357+ oslo_messaging_flags = conf.get('oslo-messaging-flags', None)
358+ if oslo_messaging_flags:
359+ ctxt['oslo_messaging_flags'] = config_flags_parser(
360+ oslo_messaging_flags)
361+
362 if not context_complete(ctxt):
363 return {}
364
365@@ -677,7 +743,14 @@
366 'endpoints': [],
367 'ext_ports': []}
368
369- for cn in self.canonical_names():
370+ cns = self.canonical_names()
371+ if cns:
372+ for cn in cns:
373+ self.configure_cert(cn)
374+ else:
375+ # Expect cert/key provided in config (currently assumed that ca
376+ # uses ip for cn)
377+ cn = resolve_address(endpoint_type=INTERNAL)
378 self.configure_cert(cn)
379
380 addresses = self.get_network_addresses()
381@@ -740,6 +813,19 @@
382
383 return ovs_ctxt
384
385+ def nuage_ctxt(self):
386+ driver = neutron_plugin_attribute(self.plugin, 'driver',
387+ self.network_manager)
388+ config = neutron_plugin_attribute(self.plugin, 'config',
389+ self.network_manager)
390+ nuage_ctxt = {'core_plugin': driver,
391+ 'neutron_plugin': 'vsp',
392+ 'neutron_security_groups': self.neutron_security_groups,
393+ 'local_ip': unit_private_ip(),
394+ 'config': config}
395+
396+ return nuage_ctxt
397+
398 def nvp_ctxt(self):
399 driver = neutron_plugin_attribute(self.plugin, 'driver',
400 self.network_manager)
401@@ -823,6 +909,8 @@
402 ctxt.update(self.n1kv_ctxt())
403 elif self.plugin == 'Calico':
404 ctxt.update(self.calico_ctxt())
405+ elif self.plugin == 'vsp':
406+ ctxt.update(self.nuage_ctxt())
407
408 alchemy_flags = config('neutron-alchemy-flags')
409 if alchemy_flags:
410@@ -833,6 +921,48 @@
411 return ctxt
412
413
414+class NeutronPortContext(OSContextGenerator):
415+ NIC_PREFIXES = ['eth', 'bond']
416+
417+ def resolve_ports(self, ports):
418+ """Resolve NICs not yet bound to bridge(s)
419+
420+ If hwaddress provided then returns resolved hwaddress otherwise NIC.
421+ """
422+ if not ports:
423+ return None
424+
425+ hwaddr_to_nic = {}
426+ hwaddr_to_ip = {}
427+ for nic in list_nics(self.NIC_PREFIXES):
428+ hwaddr = get_nic_hwaddr(nic)
429+ hwaddr_to_nic[hwaddr] = nic
430+ addresses = get_ipv4_addr(nic, fatal=False)
431+ addresses += get_ipv6_addr(iface=nic, fatal=False)
432+ hwaddr_to_ip[hwaddr] = addresses
433+
434+ resolved = []
435+ mac_regex = re.compile(r'([0-9A-F]{2}[:-]){5}([0-9A-F]{2})', re.I)
436+ for entry in ports:
437+ if re.match(mac_regex, entry):
438+ # NIC is in known NICs and does NOT hace an IP address
439+ if entry in hwaddr_to_nic and not hwaddr_to_ip[entry]:
440+ # If the nic is part of a bridge then don't use it
441+ if is_bridge_member(hwaddr_to_nic[entry]):
442+ continue
443+
444+ # Entry is a MAC address for a valid interface that doesn't
445+ # have an IP address assigned yet.
446+ resolved.append(hwaddr_to_nic[entry])
447+ else:
448+ # If the passed entry is not a MAC address, assume it's a valid
449+ # interface, and that the user put it there on purpose (we can
450+ # trust it to be the real external network).
451+ resolved.append(entry)
452+
453+ return resolved
454+
455+
456 class OSConfigFlagContext(OSContextGenerator):
457 """Provides support for user-defined config flags.
458
459@@ -1021,6 +1151,8 @@
460 for unit in related_units(rid):
461 ctxt['zmq_nonce'] = relation_get('nonce', unit, rid)
462 ctxt['zmq_host'] = relation_get('host', unit, rid)
463+ ctxt['zmq_redis_address'] = relation_get(
464+ 'zmq_redis_address', unit, rid)
465
466 return ctxt
467
468@@ -1052,3 +1184,145 @@
469 sysctl_create(sysctl_dict,
470 '/etc/sysctl.d/50-{0}.conf'.format(charm_name()))
471 return {'sysctl': sysctl_dict}
472+
473+
474+class NeutronAPIContext(OSContextGenerator):
475+ '''
476+ Inspects current neutron-plugin-api relation for neutron settings. Return
477+ defaults if it is not present.
478+ '''
479+ interfaces = ['neutron-plugin-api']
480+
481+ def __call__(self):
482+ self.neutron_defaults = {
483+ 'l2_population': {
484+ 'rel_key': 'l2-population',
485+ 'default': False,
486+ },
487+ 'overlay_network_type': {
488+ 'rel_key': 'overlay-network-type',
489+ 'default': 'gre',
490+ },
491+ 'neutron_security_groups': {
492+ 'rel_key': 'neutron-security-groups',
493+ 'default': False,
494+ },
495+ 'network_device_mtu': {
496+ 'rel_key': 'network-device-mtu',
497+ 'default': None,
498+ },
499+ 'enable_dvr': {
500+ 'rel_key': 'enable-dvr',
501+ 'default': False,
502+ },
503+ 'enable_l3ha': {
504+ 'rel_key': 'enable-l3ha',
505+ 'default': False,
506+ },
507+ }
508+ ctxt = self.get_neutron_options({})
509+ for rid in relation_ids('neutron-plugin-api'):
510+ for unit in related_units(rid):
511+ rdata = relation_get(rid=rid, unit=unit)
512+ if 'l2-population' in rdata:
513+ ctxt.update(self.get_neutron_options(rdata))
514+
515+ return ctxt
516+
517+ def get_neutron_options(self, rdata):
518+ settings = {}
519+ for nkey in self.neutron_defaults.keys():
520+ defv = self.neutron_defaults[nkey]['default']
521+ rkey = self.neutron_defaults[nkey]['rel_key']
522+ if rkey in rdata.keys():
523+ if type(defv) is bool:
524+ settings[nkey] = bool_from_string(rdata[rkey])
525+ else:
526+ settings[nkey] = rdata[rkey]
527+ else:
528+ settings[nkey] = defv
529+ return settings
530+
531+
532+class ExternalPortContext(NeutronPortContext):
533+
534+ def __call__(self):
535+ ctxt = {}
536+ ports = config('ext-port')
537+ if ports:
538+ ports = [p.strip() for p in ports.split()]
539+ ports = self.resolve_ports(ports)
540+ if ports:
541+ ctxt = {"ext_port": ports[0]}
542+ napi_settings = NeutronAPIContext()()
543+ mtu = napi_settings.get('network_device_mtu')
544+ if mtu:
545+ ctxt['ext_port_mtu'] = mtu
546+
547+ return ctxt
548+
549+
550+class DataPortContext(NeutronPortContext):
551+
552+ def __call__(self):
553+ ports = config('data-port')
554+ if ports:
555+ portmap = parse_data_port_mappings(ports)
556+ ports = portmap.values()
557+ resolved = self.resolve_ports(ports)
558+ normalized = {get_nic_hwaddr(port): port for port in resolved
559+ if port not in ports}
560+ normalized.update({port: port for port in resolved
561+ if port in ports})
562+ if resolved:
563+ return {bridge: normalized[port] for bridge, port in
564+ six.iteritems(portmap) if port in normalized.keys()}
565+
566+ return None
567+
568+
569+class PhyNICMTUContext(DataPortContext):
570+
571+ def __call__(self):
572+ ctxt = {}
573+ mappings = super(PhyNICMTUContext, self).__call__()
574+ if mappings and mappings.values():
575+ ports = mappings.values()
576+ napi_settings = NeutronAPIContext()()
577+ mtu = napi_settings.get('network_device_mtu')
578+ if mtu:
579+ ctxt["devs"] = '\\n'.join(ports)
580+ ctxt['mtu'] = mtu
581+
582+ return ctxt
583+
584+
585+class NetworkServiceContext(OSContextGenerator):
586+
587+ def __init__(self, rel_name='quantum-network-service'):
588+ self.rel_name = rel_name
589+ self.interfaces = [rel_name]
590+
591+ def __call__(self):
592+ for rid in relation_ids(self.rel_name):
593+ for unit in related_units(rid):
594+ rdata = relation_get(rid=rid, unit=unit)
595+ ctxt = {
596+ 'keystone_host': rdata.get('keystone_host'),
597+ 'service_port': rdata.get('service_port'),
598+ 'auth_port': rdata.get('auth_port'),
599+ 'service_tenant': rdata.get('service_tenant'),
600+ 'service_username': rdata.get('service_username'),
601+ 'service_password': rdata.get('service_password'),
602+ 'quantum_host': rdata.get('quantum_host'),
603+ 'quantum_port': rdata.get('quantum_port'),
604+ 'quantum_url': rdata.get('quantum_url'),
605+ 'region': rdata.get('region'),
606+ 'service_protocol':
607+ rdata.get('service_protocol') or 'http',
608+ 'auth_protocol':
609+ rdata.get('auth_protocol') or 'http',
610+ }
611+ if context_complete(ctxt):
612+ return ctxt
613+ return {}
614
615=== added directory 'hooks/charmhelpers/contrib/openstack/files'
616=== added file 'hooks/charmhelpers/contrib/openstack/files/__init__.py'
617--- hooks/charmhelpers/contrib/openstack/files/__init__.py 1970-01-01 00:00:00 +0000
618+++ hooks/charmhelpers/contrib/openstack/files/__init__.py 2015-04-16 21:56:47 +0000
619@@ -0,0 +1,18 @@
620+# Copyright 2014-2015 Canonical Limited.
621+#
622+# This file is part of charm-helpers.
623+#
624+# charm-helpers is free software: you can redistribute it and/or modify
625+# it under the terms of the GNU Lesser General Public License version 3 as
626+# published by the Free Software Foundation.
627+#
628+# charm-helpers is distributed in the hope that it will be useful,
629+# but WITHOUT ANY WARRANTY; without even the implied warranty of
630+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
631+# GNU Lesser General Public License for more details.
632+#
633+# You should have received a copy of the GNU Lesser General Public License
634+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
635+
636+# dummy __init__.py to fool syncer into thinking this is a syncable python
637+# module
638
639=== modified file 'hooks/charmhelpers/contrib/openstack/ip.py'
640--- hooks/charmhelpers/contrib/openstack/ip.py 2015-01-29 13:02:55 +0000
641+++ hooks/charmhelpers/contrib/openstack/ip.py 2015-04-16 21:56:47 +0000
642@@ -26,6 +26,8 @@
643 )
644 from charmhelpers.contrib.hahelpers.cluster import is_clustered
645
646+from functools import partial
647+
648 PUBLIC = 'public'
649 INTERNAL = 'int'
650 ADMIN = 'admin'
651@@ -107,3 +109,38 @@
652 "clustered=%s)" % (net_type, clustered))
653
654 return resolved_address
655+
656+
657+def endpoint_url(configs, url_template, port, endpoint_type=PUBLIC,
658+ override=None):
659+ """Returns the correct endpoint URL to advertise to Keystone.
660+
661+ This method provides the correct endpoint URL which should be advertised to
662+ the keystone charm for endpoint creation. This method allows for the url to
663+ be overridden to force a keystone endpoint to have specific URL for any of
664+ the defined scopes (admin, internal, public).
665+
666+ :param configs: OSTemplateRenderer config templating object to inspect
667+ for a complete https context.
668+ :param url_template: str format string for creating the url template. Only
669+ two values will be passed - the scheme+hostname
670+ returned by the canonical_url and the port.
671+ :param endpoint_type: str endpoint type to resolve.
672+ :param override: str the name of the config option which overrides the
673+ endpoint URL defined by the charm itself. None will
674+ disable any overrides (default).
675+ """
676+ if override:
677+ # Return any user-defined overrides for the keystone endpoint URL.
678+ user_value = config(override)
679+ if user_value:
680+ return user_value.strip()
681+
682+ return url_template % (canonical_url(configs, endpoint_type), port)
683+
684+
685+public_endpoint = partial(endpoint_url, endpoint_type=PUBLIC)
686+
687+internal_endpoint = partial(endpoint_url, endpoint_type=INTERNAL)
688+
689+admin_endpoint = partial(endpoint_url, endpoint_type=ADMIN)
690
691=== modified file 'hooks/charmhelpers/contrib/openstack/neutron.py'
692--- hooks/charmhelpers/contrib/openstack/neutron.py 2015-01-29 13:02:55 +0000
693+++ hooks/charmhelpers/contrib/openstack/neutron.py 2015-04-16 21:56:47 +0000
694@@ -16,6 +16,7 @@
695
696 # Various utilies for dealing with Neutron and the renaming from Quantum.
697
698+import six
699 from subprocess import check_output
700
701 from charmhelpers.core.hookenv import (
702@@ -179,6 +180,19 @@
703 'nova-api-metadata']],
704 'server_packages': ['neutron-server', 'calico-control'],
705 'server_services': ['neutron-server']
706+ },
707+ 'vsp': {
708+ 'config': '/etc/neutron/plugins/nuage/nuage_plugin.ini',
709+ 'driver': 'neutron.plugins.nuage.plugin.NuagePlugin',
710+ 'contexts': [
711+ context.SharedDBContext(user=config('neutron-database-user'),
712+ database=config('neutron-database'),
713+ relation_prefix='neutron',
714+ ssl_dir=NEUTRON_CONF_DIR)],
715+ 'services': [],
716+ 'packages': [],
717+ 'server_packages': ['neutron-server', 'neutron-plugin-nuage'],
718+ 'server_services': ['neutron-server']
719 }
720 }
721 if release >= 'icehouse':
722@@ -237,3 +251,72 @@
723 else:
724 # ensure accurate naming for all releases post-H
725 return 'neutron'
726+
727+
728+def parse_mappings(mappings):
729+ parsed = {}
730+ if mappings:
731+ mappings = mappings.split(' ')
732+ for m in mappings:
733+ p = m.partition(':')
734+ if p[1] == ':':
735+ parsed[p[0].strip()] = p[2].strip()
736+
737+ return parsed
738+
739+
740+def parse_bridge_mappings(mappings):
741+ """Parse bridge mappings.
742+
743+ Mappings must be a space-delimited list of provider:bridge mappings.
744+
745+ Returns dict of the form {provider:bridge}.
746+ """
747+ return parse_mappings(mappings)
748+
749+
750+def parse_data_port_mappings(mappings, default_bridge='br-data'):
751+ """Parse data port mappings.
752+
753+ Mappings must be a space-delimited list of bridge:port mappings.
754+
755+ Returns dict of the form {bridge:port}.
756+ """
757+ _mappings = parse_mappings(mappings)
758+ if not _mappings:
759+ if not mappings:
760+ return {}
761+
762+ # For backwards-compatibility we need to support port-only provided in
763+ # config.
764+ _mappings = {default_bridge: mappings.split(' ')[0]}
765+
766+ bridges = _mappings.keys()
767+ ports = _mappings.values()
768+ if len(set(bridges)) != len(bridges):
769+ raise Exception("It is not allowed to have more than one port "
770+ "configured on the same bridge")
771+
772+ if len(set(ports)) != len(ports):
773+ raise Exception("It is not allowed to have the same port configured "
774+ "on more than one bridge")
775+
776+ return _mappings
777+
778+
779+def parse_vlan_range_mappings(mappings):
780+ """Parse vlan range mappings.
781+
782+ Mappings must be a space-delimited list of provider:start:end mappings.
783+
784+ Returns dict of the form {provider: (start, end)}.
785+ """
786+ _mappings = parse_mappings(mappings)
787+ if not _mappings:
788+ return {}
789+
790+ mappings = {}
791+ for p, r in six.iteritems(_mappings):
792+ mappings[p] = tuple(r.split(':'))
793+
794+ return mappings
795
796=== modified file 'hooks/charmhelpers/contrib/openstack/utils.py'
797--- hooks/charmhelpers/contrib/openstack/utils.py 2015-01-29 13:02:55 +0000
798+++ hooks/charmhelpers/contrib/openstack/utils.py 2015-04-16 21:56:47 +0000
799@@ -23,12 +23,17 @@
800 import subprocess
801 import json
802 import os
803-import socket
804 import sys
805
806 import six
807 import yaml
808
809+from charmhelpers.contrib.network import ip
810+
811+from charmhelpers.core import (
812+ unitdata,
813+)
814+
815 from charmhelpers.core.hookenv import (
816 config,
817 log as juju_log,
818@@ -103,6 +108,7 @@
819 ('2.1.0', 'juno'),
820 ('2.2.0', 'juno'),
821 ('2.2.1', 'kilo'),
822+ ('2.2.2', 'kilo'),
823 ])
824
825 DEFAULT_LOOPBACK_SIZE = '5G'
826@@ -328,6 +334,21 @@
827 error_out("Invalid openstack-release specified: %s" % rel)
828
829
830+def config_value_changed(option):
831+ """
832+ Determine if config value changed since last call to this function.
833+ """
834+ hook_data = unitdata.HookData()
835+ with hook_data():
836+ db = unitdata.kv()
837+ current = config(option)
838+ saved = db.get(option)
839+ db.set(option, current)
840+ if saved is None:
841+ return False
842+ return current != saved
843+
844+
845 def save_script_rc(script_path="scripts/scriptrc", **env_vars):
846 """
847 Write an rc file in the charm-delivered directory containing
848@@ -420,77 +441,10 @@
849 else:
850 zap_disk(block_device)
851
852-
853-def is_ip(address):
854- """
855- Returns True if address is a valid IP address.
856- """
857- try:
858- # Test to see if already an IPv4 address
859- socket.inet_aton(address)
860- return True
861- except socket.error:
862- return False
863-
864-
865-def ns_query(address):
866- try:
867- import dns.resolver
868- except ImportError:
869- apt_install('python-dnspython')
870- import dns.resolver
871-
872- if isinstance(address, dns.name.Name):
873- rtype = 'PTR'
874- elif isinstance(address, six.string_types):
875- rtype = 'A'
876- else:
877- return None
878-
879- answers = dns.resolver.query(address, rtype)
880- if answers:
881- return str(answers[0])
882- return None
883-
884-
885-def get_host_ip(hostname):
886- """
887- Resolves the IP for a given hostname, or returns
888- the input if it is already an IP.
889- """
890- if is_ip(hostname):
891- return hostname
892-
893- return ns_query(hostname)
894-
895-
896-def get_hostname(address, fqdn=True):
897- """
898- Resolves hostname for given IP, or returns the input
899- if it is already a hostname.
900- """
901- if is_ip(address):
902- try:
903- import dns.reversename
904- except ImportError:
905- apt_install('python-dnspython')
906- import dns.reversename
907-
908- rev = dns.reversename.from_address(address)
909- result = ns_query(rev)
910- if not result:
911- return None
912- else:
913- result = address
914-
915- if fqdn:
916- # strip trailing .
917- if result.endswith('.'):
918- return result[:-1]
919- else:
920- return result
921- else:
922- return result.split('.')[0]
923+is_ip = ip.is_ip
924+ns_query = ip.ns_query
925+get_host_ip = ip.get_host_ip
926+get_hostname = ip.get_hostname
927
928
929 def get_matchmaker_map(mm_file='/etc/oslo/matchmaker_ring.json'):
930@@ -534,82 +488,106 @@
931
932
933 def git_install_requested():
934- """Returns true if openstack-origin-git is specified."""
935- return config('openstack-origin-git') != "None"
936+ """
937+ Returns true if openstack-origin-git is specified.
938+ """
939+ return config('openstack-origin-git') is not None
940
941
942 requirements_dir = None
943
944
945-def git_clone_and_install(file_name, core_project):
946- """Clone/install all OpenStack repos specified in yaml config file."""
947+def git_clone_and_install(projects_yaml, core_project):
948+ """
949+ Clone/install all specified OpenStack repositories.
950+
951+ The expected format of projects_yaml is:
952+ repositories:
953+ - {name: keystone,
954+ repository: 'git://git.openstack.org/openstack/keystone.git',
955+ branch: 'stable/icehouse'}
956+ - {name: requirements,
957+ repository: 'git://git.openstack.org/openstack/requirements.git',
958+ branch: 'stable/icehouse'}
959+ directory: /mnt/openstack-git
960+ http_proxy: http://squid.internal:3128
961+ https_proxy: https://squid.internal:3128
962+
963+ The directory, http_proxy, and https_proxy keys are optional.
964+ """
965 global requirements_dir
966+ parent_dir = '/mnt/openstack-git'
967
968- if file_name == "None":
969+ if not projects_yaml:
970 return
971
972- yaml_file = os.path.join(charm_dir(), file_name)
973-
974- # clone/install the requirements project first
975- installed = _git_clone_and_install_subset(yaml_file,
976- whitelist=['requirements'])
977- if 'requirements' not in installed:
978- error_out('requirements git repository must be specified')
979-
980- # clone/install all other projects except requirements and the core project
981- blacklist = ['requirements', core_project]
982- _git_clone_and_install_subset(yaml_file, blacklist=blacklist,
983- update_requirements=True)
984-
985- # clone/install the core project
986- whitelist = [core_project]
987- installed = _git_clone_and_install_subset(yaml_file, whitelist=whitelist,
988- update_requirements=True)
989- if core_project not in installed:
990- error_out('{} git repository must be specified'.format(core_project))
991-
992-
993-def _git_clone_and_install_subset(yaml_file, whitelist=[], blacklist=[],
994- update_requirements=False):
995- """Clone/install subset of OpenStack repos specified in yaml config file."""
996- global requirements_dir
997- installed = []
998-
999- with open(yaml_file, 'r') as fd:
1000- projects = yaml.load(fd)
1001- for proj, val in projects.items():
1002- # The project subset is chosen based on the following 3 rules:
1003- # 1) If project is in blacklist, we don't clone/install it, period.
1004- # 2) If whitelist is empty, we clone/install everything else.
1005- # 3) If whitelist is not empty, we clone/install everything in the
1006- # whitelist.
1007- if proj in blacklist:
1008- continue
1009- if whitelist and proj not in whitelist:
1010- continue
1011- repo = val['repository']
1012- branch = val['branch']
1013- repo_dir = _git_clone_and_install_single(repo, branch,
1014- update_requirements)
1015- if proj == 'requirements':
1016- requirements_dir = repo_dir
1017- installed.append(proj)
1018- return installed
1019-
1020-
1021-def _git_clone_and_install_single(repo, branch, update_requirements=False):
1022- """Clone and install a single git repository."""
1023- dest_parent_dir = "/mnt/openstack-git/"
1024- dest_dir = os.path.join(dest_parent_dir, os.path.basename(repo))
1025-
1026- if not os.path.exists(dest_parent_dir):
1027- juju_log('Host dir not mounted at {}. '
1028- 'Creating directory there instead.'.format(dest_parent_dir))
1029- os.mkdir(dest_parent_dir)
1030+ projects = yaml.load(projects_yaml)
1031+ _git_validate_projects_yaml(projects, core_project)
1032+
1033+ old_environ = dict(os.environ)
1034+
1035+ if 'http_proxy' in projects.keys():
1036+ os.environ['http_proxy'] = projects['http_proxy']
1037+ if 'https_proxy' in projects.keys():
1038+ os.environ['https_proxy'] = projects['https_proxy']
1039+
1040+ if 'directory' in projects.keys():
1041+ parent_dir = projects['directory']
1042+
1043+ for p in projects['repositories']:
1044+ repo = p['repository']
1045+ branch = p['branch']
1046+ if p['name'] == 'requirements':
1047+ repo_dir = _git_clone_and_install_single(repo, branch, parent_dir,
1048+ update_requirements=False)
1049+ requirements_dir = repo_dir
1050+ else:
1051+ repo_dir = _git_clone_and_install_single(repo, branch, parent_dir,
1052+ update_requirements=True)
1053+
1054+ os.environ = old_environ
1055+
1056+
1057+def _git_validate_projects_yaml(projects, core_project):
1058+ """
1059+ Validate the projects yaml.
1060+ """
1061+ _git_ensure_key_exists('repositories', projects)
1062+
1063+ for project in projects['repositories']:
1064+ _git_ensure_key_exists('name', project.keys())
1065+ _git_ensure_key_exists('repository', project.keys())
1066+ _git_ensure_key_exists('branch', project.keys())
1067+
1068+ if projects['repositories'][0]['name'] != 'requirements':
1069+ error_out('{} git repo must be specified first'.format('requirements'))
1070+
1071+ if projects['repositories'][-1]['name'] != core_project:
1072+ error_out('{} git repo must be specified last'.format(core_project))
1073+
1074+
1075+def _git_ensure_key_exists(key, keys):
1076+ """
1077+ Ensure that key exists in keys.
1078+ """
1079+ if key not in keys:
1080+ error_out('openstack-origin-git key \'{}\' is missing'.format(key))
1081+
1082+
1083+def _git_clone_and_install_single(repo, branch, parent_dir, update_requirements):
1084+ """
1085+ Clone and install a single git repository.
1086+ """
1087+ dest_dir = os.path.join(parent_dir, os.path.basename(repo))
1088+
1089+ if not os.path.exists(parent_dir):
1090+ juju_log('Directory already exists at {}. '
1091+ 'No need to create directory.'.format(parent_dir))
1092+ os.mkdir(parent_dir)
1093
1094 if not os.path.exists(dest_dir):
1095 juju_log('Cloning git repo: {}, branch: {}'.format(repo, branch))
1096- repo_dir = install_remote(repo, dest=dest_parent_dir, branch=branch)
1097+ repo_dir = install_remote(repo, dest=parent_dir, branch=branch)
1098 else:
1099 repo_dir = dest_dir
1100
1101@@ -626,16 +604,39 @@
1102
1103
1104 def _git_update_requirements(package_dir, reqs_dir):
1105- """Update from global requirements.
1106+ """
1107+ Update from global requirements.
1108
1109- Update an OpenStack git directory's requirements.txt and
1110- test-requirements.txt from global-requirements.txt."""
1111+ Update an OpenStack git directory's requirements.txt and
1112+ test-requirements.txt from global-requirements.txt.
1113+ """
1114 orig_dir = os.getcwd()
1115 os.chdir(reqs_dir)
1116- cmd = "python update.py {}".format(package_dir)
1117+ cmd = ['python', 'update.py', package_dir]
1118 try:
1119- subprocess.check_call(cmd.split(' '))
1120+ subprocess.check_call(cmd)
1121 except subprocess.CalledProcessError:
1122 package = os.path.basename(package_dir)
1123 error_out("Error updating {} from global-requirements.txt".format(package))
1124 os.chdir(orig_dir)
1125+
1126+
1127+def git_src_dir(projects_yaml, project):
1128+ """
1129+ Return the directory where the specified project's source is located.
1130+ """
1131+ parent_dir = '/mnt/openstack-git'
1132+
1133+ if not projects_yaml:
1134+ return
1135+
1136+ projects = yaml.load(projects_yaml)
1137+
1138+ if 'directory' in projects.keys():
1139+ parent_dir = projects['directory']
1140+
1141+ for p in projects['repositories']:
1142+ if p['name'] == project:
1143+ return os.path.join(parent_dir, os.path.basename(p['repository']))
1144+
1145+ return None
1146
1147=== modified file 'hooks/charmhelpers/contrib/python/packages.py'
1148--- hooks/charmhelpers/contrib/python/packages.py 2015-01-29 13:15:20 +0000
1149+++ hooks/charmhelpers/contrib/python/packages.py 2015-04-16 21:56:47 +0000
1150@@ -17,8 +17,6 @@
1151 # You should have received a copy of the GNU Lesser General Public License
1152 # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1153
1154-__author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>"
1155-
1156 from charmhelpers.fetch import apt_install, apt_update
1157 from charmhelpers.core.hookenv import log
1158
1159@@ -29,6 +27,8 @@
1160 apt_install('python-pip')
1161 from pip import main as pip_execute
1162
1163+__author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>"
1164+
1165
1166 def parse_options(given, available):
1167 """Given a set of options, check if available"""
1168
1169=== modified file 'hooks/charmhelpers/core/fstab.py'
1170--- hooks/charmhelpers/core/fstab.py 2015-01-29 13:02:55 +0000
1171+++ hooks/charmhelpers/core/fstab.py 2015-04-16 21:56:47 +0000
1172@@ -17,11 +17,11 @@
1173 # You should have received a copy of the GNU Lesser General Public License
1174 # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1175
1176-__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
1177-
1178 import io
1179 import os
1180
1181+__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
1182+
1183
1184 class Fstab(io.FileIO):
1185 """This class extends file in order to implement a file reader/writer
1186@@ -77,7 +77,7 @@
1187 for line in self.readlines():
1188 line = line.decode('us-ascii')
1189 try:
1190- if line.strip() and not line.startswith("#"):
1191+ if line.strip() and not line.strip().startswith("#"):
1192 yield self._hydrate_entry(line)
1193 except ValueError:
1194 pass
1195@@ -104,7 +104,7 @@
1196
1197 found = False
1198 for index, line in enumerate(lines):
1199- if not line.startswith("#"):
1200+ if line.strip() and not line.strip().startswith("#"):
1201 if self._hydrate_entry(line) == entry:
1202 found = True
1203 break
1204
1205=== modified file 'hooks/charmhelpers/core/hookenv.py'
1206--- hooks/charmhelpers/core/hookenv.py 2015-01-29 13:02:55 +0000
1207+++ hooks/charmhelpers/core/hookenv.py 2015-04-16 21:56:47 +0000
1208@@ -20,11 +20,13 @@
1209 # Authors:
1210 # Charm Helpers Developers <juju@lists.ubuntu.com>
1211
1212+from __future__ import print_function
1213 import os
1214 import json
1215 import yaml
1216 import subprocess
1217 import sys
1218+import errno
1219 from subprocess import CalledProcessError
1220
1221 import six
1222@@ -87,7 +89,18 @@
1223 if not isinstance(message, six.string_types):
1224 message = repr(message)
1225 command += [message]
1226- subprocess.call(command)
1227+ # Missing juju-log should not cause failures in unit tests
1228+ # Send log output to stderr
1229+ try:
1230+ subprocess.call(command)
1231+ except OSError as e:
1232+ if e.errno == errno.ENOENT:
1233+ if level:
1234+ message = "{}: {}".format(level, message)
1235+ message = "juju-log: {}".format(message)
1236+ print(message, file=sys.stderr)
1237+ else:
1238+ raise
1239
1240
1241 class Serializable(UserDict):
1242@@ -566,3 +579,29 @@
1243 def charm_dir():
1244 """Return the root directory of the current charm"""
1245 return os.environ.get('CHARM_DIR')
1246+
1247+
1248+@cached
1249+def action_get(key=None):
1250+ """Gets the value of an action parameter, or all key/value param pairs"""
1251+ cmd = ['action-get']
1252+ if key is not None:
1253+ cmd.append(key)
1254+ cmd.append('--format=json')
1255+ action_data = json.loads(subprocess.check_output(cmd).decode('UTF-8'))
1256+ return action_data
1257+
1258+
1259+def action_set(values):
1260+ """Sets the values to be returned after the action finishes"""
1261+ cmd = ['action-set']
1262+ for k, v in list(values.items()):
1263+ cmd.append('{}={}'.format(k, v))
1264+ subprocess.check_call(cmd)
1265+
1266+
1267+def action_fail(message):
1268+ """Sets the action status to failed and sets the error message.
1269+
1270+ The results set by action_set are preserved."""
1271+ subprocess.check_call(['action-fail', message])
1272
1273=== modified file 'hooks/charmhelpers/core/host.py'
1274--- hooks/charmhelpers/core/host.py 2015-01-29 13:02:55 +0000
1275+++ hooks/charmhelpers/core/host.py 2015-04-16 21:56:47 +0000
1276@@ -191,11 +191,11 @@
1277
1278
1279 def write_file(path, content, owner='root', group='root', perms=0o444):
1280- """Create or overwrite a file with the contents of a string"""
1281+ """Create or overwrite a file with the contents of a byte string."""
1282 log("Writing file {} {}:{} {:o}".format(path, owner, group, perms))
1283 uid = pwd.getpwnam(owner).pw_uid
1284 gid = grp.getgrnam(group).gr_gid
1285- with open(path, 'w') as target:
1286+ with open(path, 'wb') as target:
1287 os.fchown(target.fileno(), uid, gid)
1288 os.fchmod(target.fileno(), perms)
1289 target.write(content)
1290@@ -305,11 +305,11 @@
1291 ceph_client_changed function.
1292 """
1293 def wrap(f):
1294- def wrapped_f(*args):
1295+ def wrapped_f(*args, **kwargs):
1296 checksums = {}
1297 for path in restart_map:
1298 checksums[path] = file_hash(path)
1299- f(*args)
1300+ f(*args, **kwargs)
1301 restarts = []
1302 for path in restart_map:
1303 if checksums[path] != file_hash(path):
1304@@ -339,12 +339,16 @@
1305 def pwgen(length=None):
1306 """Generate a random pasword."""
1307 if length is None:
1308+ # A random length is ok to use a weak PRNG
1309 length = random.choice(range(35, 45))
1310 alphanumeric_chars = [
1311 l for l in (string.ascii_letters + string.digits)
1312 if l not in 'l0QD1vAEIOUaeiou']
1313+ # Use a crypto-friendly PRNG (e.g. /dev/urandom) for making the
1314+ # actual password
1315+ random_generator = random.SystemRandom()
1316 random_chars = [
1317- random.choice(alphanumeric_chars) for _ in range(length)]
1318+ random_generator.choice(alphanumeric_chars) for _ in range(length)]
1319 return(''.join(random_chars))
1320
1321
1322@@ -361,7 +365,7 @@
1323 ip_output = (line for line in ip_output if line)
1324 for line in ip_output:
1325 if line.split()[1].startswith(int_type):
1326- matched = re.search('.*: (bond[0-9]+\.[0-9]+)@.*', line)
1327+ matched = re.search('.*: (' + int_type + r'[0-9]+\.[0-9]+)@.*', line)
1328 if matched:
1329 interface = matched.groups()[0]
1330 else:
1331
1332=== modified file 'hooks/charmhelpers/core/services/helpers.py'
1333--- hooks/charmhelpers/core/services/helpers.py 2015-01-29 13:02:55 +0000
1334+++ hooks/charmhelpers/core/services/helpers.py 2015-04-16 21:56:47 +0000
1335@@ -45,12 +45,14 @@
1336 """
1337 name = None
1338 interface = None
1339- required_keys = []
1340
1341 def __init__(self, name=None, additional_required_keys=None):
1342+ if not hasattr(self, 'required_keys'):
1343+ self.required_keys = []
1344+
1345 if name is not None:
1346 self.name = name
1347- if additional_required_keys is not None:
1348+ if additional_required_keys:
1349 self.required_keys.extend(additional_required_keys)
1350 self.get_data()
1351
1352@@ -134,7 +136,10 @@
1353 """
1354 name = 'db'
1355 interface = 'mysql'
1356- required_keys = ['host', 'user', 'password', 'database']
1357+
1358+ def __init__(self, *args, **kwargs):
1359+ self.required_keys = ['host', 'user', 'password', 'database']
1360+ RelationContext.__init__(self, *args, **kwargs)
1361
1362
1363 class HttpRelation(RelationContext):
1364@@ -146,7 +151,10 @@
1365 """
1366 name = 'website'
1367 interface = 'http'
1368- required_keys = ['host', 'port']
1369+
1370+ def __init__(self, *args, **kwargs):
1371+ self.required_keys = ['host', 'port']
1372+ RelationContext.__init__(self, *args, **kwargs)
1373
1374 def provide_data(self):
1375 return {
1376
1377=== added file 'hooks/charmhelpers/core/strutils.py'
1378--- hooks/charmhelpers/core/strutils.py 1970-01-01 00:00:00 +0000
1379+++ hooks/charmhelpers/core/strutils.py 2015-04-16 21:56:47 +0000
1380@@ -0,0 +1,42 @@
1381+#!/usr/bin/env python
1382+# -*- coding: utf-8 -*-
1383+
1384+# Copyright 2014-2015 Canonical Limited.
1385+#
1386+# This file is part of charm-helpers.
1387+#
1388+# charm-helpers is free software: you can redistribute it and/or modify
1389+# it under the terms of the GNU Lesser General Public License version 3 as
1390+# published by the Free Software Foundation.
1391+#
1392+# charm-helpers is distributed in the hope that it will be useful,
1393+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1394+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1395+# GNU Lesser General Public License for more details.
1396+#
1397+# You should have received a copy of the GNU Lesser General Public License
1398+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1399+
1400+import six
1401+
1402+
1403+def bool_from_string(value):
1404+ """Interpret string value as boolean.
1405+
1406+ Returns True if value translates to True otherwise False.
1407+ """
1408+ if isinstance(value, six.string_types):
1409+ value = six.text_type(value)
1410+ else:
1411+ msg = "Unable to interpret non-string value '%s' as boolean" % (value)
1412+ raise ValueError(msg)
1413+
1414+ value = value.strip().lower()
1415+
1416+ if value in ['y', 'yes', 'true', 't', 'on']:
1417+ return True
1418+ elif value in ['n', 'no', 'false', 'f', 'off']:
1419+ return False
1420+
1421+ msg = "Unable to interpret string value '%s' as boolean" % (value)
1422+ raise ValueError(msg)
1423
1424=== modified file 'hooks/charmhelpers/core/sysctl.py'
1425--- hooks/charmhelpers/core/sysctl.py 2015-01-29 13:02:55 +0000
1426+++ hooks/charmhelpers/core/sysctl.py 2015-04-16 21:56:47 +0000
1427@@ -17,8 +17,6 @@
1428 # You should have received a copy of the GNU Lesser General Public License
1429 # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1430
1431-__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
1432-
1433 import yaml
1434
1435 from subprocess import check_call
1436@@ -26,25 +24,33 @@
1437 from charmhelpers.core.hookenv import (
1438 log,
1439 DEBUG,
1440+ ERROR,
1441 )
1442
1443+__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
1444+
1445
1446 def create(sysctl_dict, sysctl_file):
1447 """Creates a sysctl.conf file from a YAML associative array
1448
1449- :param sysctl_dict: a dict of sysctl options eg { 'kernel.max_pid': 1337 }
1450- :type sysctl_dict: dict
1451+ :param sysctl_dict: a YAML-formatted string of sysctl options eg "{ 'kernel.max_pid': 1337 }"
1452+ :type sysctl_dict: str
1453 :param sysctl_file: path to the sysctl file to be saved
1454 :type sysctl_file: str or unicode
1455 :returns: None
1456 """
1457- sysctl_dict = yaml.load(sysctl_dict)
1458+ try:
1459+ sysctl_dict_parsed = yaml.safe_load(sysctl_dict)
1460+ except yaml.YAMLError:
1461+ log("Error parsing YAML sysctl_dict: {}".format(sysctl_dict),
1462+ level=ERROR)
1463+ return
1464
1465 with open(sysctl_file, "w") as fd:
1466- for key, value in sysctl_dict.items():
1467+ for key, value in sysctl_dict_parsed.items():
1468 fd.write("{}={}\n".format(key, value))
1469
1470- log("Updating sysctl_file: %s values: %s" % (sysctl_file, sysctl_dict),
1471+ log("Updating sysctl_file: %s values: %s" % (sysctl_file, sysctl_dict_parsed),
1472 level=DEBUG)
1473
1474 check_call(["sysctl", "-p", sysctl_file])
1475
1476=== modified file 'hooks/charmhelpers/core/templating.py'
1477--- hooks/charmhelpers/core/templating.py 2015-01-29 13:02:55 +0000
1478+++ hooks/charmhelpers/core/templating.py 2015-04-16 21:56:47 +0000
1479@@ -21,7 +21,7 @@
1480
1481
1482 def render(source, target, context, owner='root', group='root',
1483- perms=0o444, templates_dir=None):
1484+ perms=0o444, templates_dir=None, encoding='UTF-8'):
1485 """
1486 Render a template.
1487
1488@@ -64,5 +64,5 @@
1489 level=hookenv.ERROR)
1490 raise e
1491 content = template.render(context)
1492- host.mkdir(os.path.dirname(target), owner, group)
1493- host.write_file(target, content, owner, group, perms)
1494+ host.mkdir(os.path.dirname(target), owner, group, perms=0o755)
1495+ host.write_file(target, content.encode(encoding), owner, group, perms)
1496
1497=== added file 'hooks/charmhelpers/core/unitdata.py'
1498--- hooks/charmhelpers/core/unitdata.py 1970-01-01 00:00:00 +0000
1499+++ hooks/charmhelpers/core/unitdata.py 2015-04-16 21:56:47 +0000
1500@@ -0,0 +1,477 @@
1501+#!/usr/bin/env python
1502+# -*- coding: utf-8 -*-
1503+#
1504+# Copyright 2014-2015 Canonical Limited.
1505+#
1506+# This file is part of charm-helpers.
1507+#
1508+# charm-helpers is free software: you can redistribute it and/or modify
1509+# it under the terms of the GNU Lesser General Public License version 3 as
1510+# published by the Free Software Foundation.
1511+#
1512+# charm-helpers is distributed in the hope that it will be useful,
1513+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1514+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1515+# GNU Lesser General Public License for more details.
1516+#
1517+# You should have received a copy of the GNU Lesser General Public License
1518+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1519+#
1520+#
1521+# Authors:
1522+# Kapil Thangavelu <kapil.foss@gmail.com>
1523+#
1524+"""
1525+Intro
1526+-----
1527+
1528+A simple way to store state in units. This provides a key value
1529+storage with support for versioned, transactional operation,
1530+and can calculate deltas from previous values to simplify unit logic
1531+when processing changes.
1532+
1533+
1534+Hook Integration
1535+----------------
1536+
1537+There are several extant frameworks for hook execution, including
1538+
1539+ - charmhelpers.core.hookenv.Hooks
1540+ - charmhelpers.core.services.ServiceManager
1541+
1542+The storage classes are framework agnostic, one simple integration is
1543+via the HookData contextmanager. It will record the current hook
1544+execution environment (including relation data, config data, etc.),
1545+setup a transaction and allow easy access to the changes from
1546+previously seen values. One consequence of the integration is the
1547+reservation of particular keys ('rels', 'unit', 'env', 'config',
1548+'charm_revisions') for their respective values.
1549+
1550+Here's a fully worked integration example using hookenv.Hooks::
1551+
1552+ from charmhelper.core import hookenv, unitdata
1553+
1554+ hook_data = unitdata.HookData()
1555+ db = unitdata.kv()
1556+ hooks = hookenv.Hooks()
1557+
1558+ @hooks.hook
1559+ def config_changed():
1560+ # Print all changes to configuration from previously seen
1561+ # values.
1562+ for changed, (prev, cur) in hook_data.conf.items():
1563+ print('config changed', changed,
1564+ 'previous value', prev,
1565+ 'current value', cur)
1566+
1567+ # Get some unit specific bookeeping
1568+ if not db.get('pkg_key'):
1569+ key = urllib.urlopen('https://example.com/pkg_key').read()
1570+ db.set('pkg_key', key)
1571+
1572+ # Directly access all charm config as a mapping.
1573+ conf = db.getrange('config', True)
1574+
1575+ # Directly access all relation data as a mapping
1576+ rels = db.getrange('rels', True)
1577+
1578+ if __name__ == '__main__':
1579+ with hook_data():
1580+ hook.execute()
1581+
1582+
1583+A more basic integration is via the hook_scope context manager which simply
1584+manages transaction scope (and records hook name, and timestamp)::
1585+
1586+ >>> from unitdata import kv
1587+ >>> db = kv()
1588+ >>> with db.hook_scope('install'):
1589+ ... # do work, in transactional scope.
1590+ ... db.set('x', 1)
1591+ >>> db.get('x')
1592+ 1
1593+
1594+
1595+Usage
1596+-----
1597+
1598+Values are automatically json de/serialized to preserve basic typing
1599+and complex data struct capabilities (dicts, lists, ints, booleans, etc).
1600+
1601+Individual values can be manipulated via get/set::
1602+
1603+ >>> kv.set('y', True)
1604+ >>> kv.get('y')
1605+ True
1606+
1607+ # We can set complex values (dicts, lists) as a single key.
1608+ >>> kv.set('config', {'a': 1, 'b': True'})
1609+
1610+ # Also supports returning dictionaries as a record which
1611+ # provides attribute access.
1612+ >>> config = kv.get('config', record=True)
1613+ >>> config.b
1614+ True
1615+
1616+
1617+Groups of keys can be manipulated with update/getrange::
1618+
1619+ >>> kv.update({'z': 1, 'y': 2}, prefix="gui.")
1620+ >>> kv.getrange('gui.', strip=True)
1621+ {'z': 1, 'y': 2}
1622+
1623+When updating values, its very helpful to understand which values
1624+have actually changed and how have they changed. The storage
1625+provides a delta method to provide for this::
1626+
1627+ >>> data = {'debug': True, 'option': 2}
1628+ >>> delta = kv.delta(data, 'config.')
1629+ >>> delta.debug.previous
1630+ None
1631+ >>> delta.debug.current
1632+ True
1633+ >>> delta
1634+ {'debug': (None, True), 'option': (None, 2)}
1635+
1636+Note the delta method does not persist the actual change, it needs to
1637+be explicitly saved via 'update' method::
1638+
1639+ >>> kv.update(data, 'config.')
1640+
1641+Values modified in the context of a hook scope retain historical values
1642+associated to the hookname.
1643+
1644+ >>> with db.hook_scope('config-changed'):
1645+ ... db.set('x', 42)
1646+ >>> db.gethistory('x')
1647+ [(1, u'x', 1, u'install', u'2015-01-21T16:49:30.038372'),
1648+ (2, u'x', 42, u'config-changed', u'2015-01-21T16:49:30.038786')]
1649+
1650+"""
1651+
1652+import collections
1653+import contextlib
1654+import datetime
1655+import json
1656+import os
1657+import pprint
1658+import sqlite3
1659+import sys
1660+
1661+__author__ = 'Kapil Thangavelu <kapil.foss@gmail.com>'
1662+
1663+
1664+class Storage(object):
1665+ """Simple key value database for local unit state within charms.
1666+
1667+ Modifications are automatically committed at hook exit. That's
1668+ currently regardless of exit code.
1669+
1670+ To support dicts, lists, integer, floats, and booleans values
1671+ are automatically json encoded/decoded.
1672+ """
1673+ def __init__(self, path=None):
1674+ self.db_path = path
1675+ if path is None:
1676+ self.db_path = os.path.join(
1677+ os.environ.get('CHARM_DIR', ''), '.unit-state.db')
1678+ self.conn = sqlite3.connect('%s' % self.db_path)
1679+ self.cursor = self.conn.cursor()
1680+ self.revision = None
1681+ self._closed = False
1682+ self._init()
1683+
1684+ def close(self):
1685+ if self._closed:
1686+ return
1687+ self.flush(False)
1688+ self.cursor.close()
1689+ self.conn.close()
1690+ self._closed = True
1691+
1692+ def _scoped_query(self, stmt, params=None):
1693+ if params is None:
1694+ params = []
1695+ return stmt, params
1696+
1697+ def get(self, key, default=None, record=False):
1698+ self.cursor.execute(
1699+ *self._scoped_query(
1700+ 'select data from kv where key=?', [key]))
1701+ result = self.cursor.fetchone()
1702+ if not result:
1703+ return default
1704+ if record:
1705+ return Record(json.loads(result[0]))
1706+ return json.loads(result[0])
1707+
1708+ def getrange(self, key_prefix, strip=False):
1709+ stmt = "select key, data from kv where key like '%s%%'" % key_prefix
1710+ self.cursor.execute(*self._scoped_query(stmt))
1711+ result = self.cursor.fetchall()
1712+
1713+ if not result:
1714+ return None
1715+ if not strip:
1716+ key_prefix = ''
1717+ return dict([
1718+ (k[len(key_prefix):], json.loads(v)) for k, v in result])
1719+
1720+ def update(self, mapping, prefix=""):
1721+ for k, v in mapping.items():
1722+ self.set("%s%s" % (prefix, k), v)
1723+
1724+ def unset(self, key):
1725+ self.cursor.execute('delete from kv where key=?', [key])
1726+ if self.revision and self.cursor.rowcount:
1727+ self.cursor.execute(
1728+ 'insert into kv_revisions values (?, ?, ?)',
1729+ [key, self.revision, json.dumps('DELETED')])
1730+
1731+ def set(self, key, value):
1732+ serialized = json.dumps(value)
1733+
1734+ self.cursor.execute(
1735+ 'select data from kv where key=?', [key])
1736+ exists = self.cursor.fetchone()
1737+
1738+ # Skip mutations to the same value
1739+ if exists:
1740+ if exists[0] == serialized:
1741+ return value
1742+
1743+ if not exists:
1744+ self.cursor.execute(
1745+ 'insert into kv (key, data) values (?, ?)',
1746+ (key, serialized))
1747+ else:
1748+ self.cursor.execute('''
1749+ update kv
1750+ set data = ?
1751+ where key = ?''', [serialized, key])
1752+
1753+ # Save
1754+ if not self.revision:
1755+ return value
1756+
1757+ self.cursor.execute(
1758+ 'select 1 from kv_revisions where key=? and revision=?',
1759+ [key, self.revision])
1760+ exists = self.cursor.fetchone()
1761+
1762+ if not exists:
1763+ self.cursor.execute(
1764+ '''insert into kv_revisions (
1765+ revision, key, data) values (?, ?, ?)''',
1766+ (self.revision, key, serialized))
1767+ else:
1768+ self.cursor.execute(
1769+ '''
1770+ update kv_revisions
1771+ set data = ?
1772+ where key = ?
1773+ and revision = ?''',
1774+ [serialized, key, self.revision])
1775+
1776+ return value
1777+
1778+ def delta(self, mapping, prefix):
1779+ """
1780+ return a delta containing values that have changed.
1781+ """
1782+ previous = self.getrange(prefix, strip=True)
1783+ if not previous:
1784+ pk = set()
1785+ else:
1786+ pk = set(previous.keys())
1787+ ck = set(mapping.keys())
1788+ delta = DeltaSet()
1789+
1790+ # added
1791+ for k in ck.difference(pk):
1792+ delta[k] = Delta(None, mapping[k])
1793+
1794+ # removed
1795+ for k in pk.difference(ck):
1796+ delta[k] = Delta(previous[k], None)
1797+
1798+ # changed
1799+ for k in pk.intersection(ck):
1800+ c = mapping[k]
1801+ p = previous[k]
1802+ if c != p:
1803+ delta[k] = Delta(p, c)
1804+
1805+ return delta
1806+
1807+ @contextlib.contextmanager
1808+ def hook_scope(self, name=""):
1809+ """Scope all future interactions to the current hook execution
1810+ revision."""
1811+ assert not self.revision
1812+ self.cursor.execute(
1813+ 'insert into hooks (hook, date) values (?, ?)',
1814+ (name or sys.argv[0],
1815+ datetime.datetime.utcnow().isoformat()))
1816+ self.revision = self.cursor.lastrowid
1817+ try:
1818+ yield self.revision
1819+ self.revision = None
1820+ except:
1821+ self.flush(False)
1822+ self.revision = None
1823+ raise
1824+ else:
1825+ self.flush()
1826+
1827+ def flush(self, save=True):
1828+ if save:
1829+ self.conn.commit()
1830+ elif self._closed:
1831+ return
1832+ else:
1833+ self.conn.rollback()
1834+
1835+ def _init(self):
1836+ self.cursor.execute('''
1837+ create table if not exists kv (
1838+ key text,
1839+ data text,
1840+ primary key (key)
1841+ )''')
1842+ self.cursor.execute('''
1843+ create table if not exists kv_revisions (
1844+ key text,
1845+ revision integer,
1846+ data text,
1847+ primary key (key, revision)
1848+ )''')
1849+ self.cursor.execute('''
1850+ create table if not exists hooks (
1851+ version integer primary key autoincrement,
1852+ hook text,
1853+ date text
1854+ )''')
1855+ self.conn.commit()
1856+
1857+ def gethistory(self, key, deserialize=False):
1858+ self.cursor.execute(
1859+ '''
1860+ select kv.revision, kv.key, kv.data, h.hook, h.date
1861+ from kv_revisions kv,
1862+ hooks h
1863+ where kv.key=?
1864+ and kv.revision = h.version
1865+ ''', [key])
1866+ if deserialize is False:
1867+ return self.cursor.fetchall()
1868+ return map(_parse_history, self.cursor.fetchall())
1869+
1870+ def debug(self, fh=sys.stderr):
1871+ self.cursor.execute('select * from kv')
1872+ pprint.pprint(self.cursor.fetchall(), stream=fh)
1873+ self.cursor.execute('select * from kv_revisions')
1874+ pprint.pprint(self.cursor.fetchall(), stream=fh)
1875+
1876+
1877+def _parse_history(d):
1878+ return (d[0], d[1], json.loads(d[2]), d[3],
1879+ datetime.datetime.strptime(d[-1], "%Y-%m-%dT%H:%M:%S.%f"))
1880+
1881+
1882+class HookData(object):
1883+ """Simple integration for existing hook exec frameworks.
1884+
1885+ Records all unit information, and stores deltas for processing
1886+ by the hook.
1887+
1888+ Sample::
1889+
1890+ from charmhelper.core import hookenv, unitdata
1891+
1892+ changes = unitdata.HookData()
1893+ db = unitdata.kv()
1894+ hooks = hookenv.Hooks()
1895+
1896+ @hooks.hook
1897+ def config_changed():
1898+ # View all changes to configuration
1899+ for changed, (prev, cur) in changes.conf.items():
1900+ print('config changed', changed,
1901+ 'previous value', prev,
1902+ 'current value', cur)
1903+
1904+ # Get some unit specific bookeeping
1905+ if not db.get('pkg_key'):
1906+ key = urllib.urlopen('https://example.com/pkg_key').read()
1907+ db.set('pkg_key', key)
1908+
1909+ if __name__ == '__main__':
1910+ with changes():
1911+ hook.execute()
1912+
1913+ """
1914+ def __init__(self):
1915+ self.kv = kv()
1916+ self.conf = None
1917+ self.rels = None
1918+
1919+ @contextlib.contextmanager
1920+ def __call__(self):
1921+ from charmhelpers.core import hookenv
1922+ hook_name = hookenv.hook_name()
1923+
1924+ with self.kv.hook_scope(hook_name):
1925+ self._record_charm_version(hookenv.charm_dir())
1926+ delta_config, delta_relation = self._record_hook(hookenv)
1927+ yield self.kv, delta_config, delta_relation
1928+
1929+ def _record_charm_version(self, charm_dir):
1930+ # Record revisions.. charm revisions are meaningless
1931+ # to charm authors as they don't control the revision.
1932+ # so logic dependnent on revision is not particularly
1933+ # useful, however it is useful for debugging analysis.
1934+ charm_rev = open(
1935+ os.path.join(charm_dir, 'revision')).read().strip()
1936+ charm_rev = charm_rev or '0'
1937+ revs = self.kv.get('charm_revisions', [])
1938+ if charm_rev not in revs:
1939+ revs.append(charm_rev.strip() or '0')
1940+ self.kv.set('charm_revisions', revs)
1941+
1942+ def _record_hook(self, hookenv):
1943+ data = hookenv.execution_environment()
1944+ self.conf = conf_delta = self.kv.delta(data['conf'], 'config')
1945+ self.rels = rels_delta = self.kv.delta(data['rels'], 'rels')
1946+ self.kv.set('env', dict(data['env']))
1947+ self.kv.set('unit', data['unit'])
1948+ self.kv.set('relid', data.get('relid'))
1949+ return conf_delta, rels_delta
1950+
1951+
1952+class Record(dict):
1953+
1954+ __slots__ = ()
1955+
1956+ def __getattr__(self, k):
1957+ if k in self:
1958+ return self[k]
1959+ raise AttributeError(k)
1960+
1961+
1962+class DeltaSet(Record):
1963+
1964+ __slots__ = ()
1965+
1966+
1967+Delta = collections.namedtuple('Delta', ['previous', 'current'])
1968+
1969+
1970+_KV = None
1971+
1972+
1973+def kv():
1974+ global _KV
1975+ if _KV is None:
1976+ _KV = Storage()
1977+ return _KV
1978
1979=== modified file 'hooks/charmhelpers/fetch/archiveurl.py'
1980--- hooks/charmhelpers/fetch/archiveurl.py 2015-01-29 13:02:55 +0000
1981+++ hooks/charmhelpers/fetch/archiveurl.py 2015-04-16 21:56:47 +0000
1982@@ -18,6 +18,16 @@
1983 import hashlib
1984 import re
1985
1986+from charmhelpers.fetch import (
1987+ BaseFetchHandler,
1988+ UnhandledSource
1989+)
1990+from charmhelpers.payload.archive import (
1991+ get_archive_handler,
1992+ extract,
1993+)
1994+from charmhelpers.core.host import mkdir, check_hash
1995+
1996 import six
1997 if six.PY3:
1998 from urllib.request import (
1999@@ -35,16 +45,6 @@
2000 )
2001 from urlparse import urlparse, urlunparse, parse_qs
2002
2003-from charmhelpers.fetch import (
2004- BaseFetchHandler,
2005- UnhandledSource
2006-)
2007-from charmhelpers.payload.archive import (
2008- get_archive_handler,
2009- extract,
2010-)
2011-from charmhelpers.core.host import mkdir, check_hash
2012-
2013
2014 def splituser(host):
2015 '''urllib.splituser(), but six's support of this seems broken'''
2016
2017=== modified file 'hooks/charmhelpers/fetch/giturl.py'
2018--- hooks/charmhelpers/fetch/giturl.py 2015-01-29 13:02:55 +0000
2019+++ hooks/charmhelpers/fetch/giturl.py 2015-04-16 21:56:47 +0000
2020@@ -32,7 +32,7 @@
2021 apt_install("python-git")
2022 from git import Repo
2023
2024-from git.exc import GitCommandError
2025+from git.exc import GitCommandError # noqa E402
2026
2027
2028 class GitUrlFetchHandler(BaseFetchHandler):
2029
2030=== modified file 'tests/charmhelpers/contrib/amulet/utils.py'
2031--- tests/charmhelpers/contrib/amulet/utils.py 2015-01-29 13:02:55 +0000
2032+++ tests/charmhelpers/contrib/amulet/utils.py 2015-04-16 21:56:47 +0000
2033@@ -118,6 +118,9 @@
2034 longs, or can be a function that evaluate a variable and returns a
2035 bool.
2036 """
2037+ self.log.debug('actual: {}'.format(repr(actual)))
2038+ self.log.debug('expected: {}'.format(repr(expected)))
2039+
2040 for k, v in six.iteritems(expected):
2041 if k in actual:
2042 if (isinstance(v, six.string_types) or
2043@@ -134,7 +137,6 @@
2044 def validate_relation_data(self, sentry_unit, relation, expected):
2045 """Validate actual relation data based on expected relation data."""
2046 actual = sentry_unit.relation(relation[0], relation[1])
2047- self.log.debug('actual: {}'.format(repr(actual)))
2048 return self._validate_dict_data(expected, actual)
2049
2050 def _validate_list_data(self, expected, actual):
2051@@ -169,8 +171,13 @@
2052 cmd = 'pgrep -o -f {}'.format(service)
2053 else:
2054 cmd = 'pgrep -o {}'.format(service)
2055- proc_dir = '/proc/{}'.format(sentry_unit.run(cmd)[0].strip())
2056- return self._get_dir_mtime(sentry_unit, proc_dir)
2057+ cmd = cmd + ' | grep -v pgrep || exit 0'
2058+ cmd_out = sentry_unit.run(cmd)
2059+ self.log.debug('CMDout: ' + str(cmd_out))
2060+ if cmd_out[0]:
2061+ self.log.debug('Pid for %s %s' % (service, str(cmd_out[0])))
2062+ proc_dir = '/proc/{}'.format(cmd_out[0].strip())
2063+ return self._get_dir_mtime(sentry_unit, proc_dir)
2064
2065 def service_restarted(self, sentry_unit, service, filename,
2066 pgrep_full=False, sleep_time=20):
2067@@ -187,6 +194,121 @@
2068 else:
2069 return False
2070
2071+ def service_restarted_since(self, sentry_unit, mtime, service,
2072+ pgrep_full=False, sleep_time=20,
2073+ retry_count=2):
2074+ """Check if service was been started after a given time.
2075+
2076+ Args:
2077+ sentry_unit (sentry): The sentry unit to check for the service on
2078+ mtime (float): The epoch time to check against
2079+ service (string): service name to look for in process table
2080+ pgrep_full (boolean): Use full command line search mode with pgrep
2081+ sleep_time (int): Seconds to sleep before looking for process
2082+ retry_count (int): If service is not found, how many times to retry
2083+
2084+ Returns:
2085+ bool: True if service found and its start time it newer than mtime,
2086+ False if service is older than mtime or if service was
2087+ not found.
2088+ """
2089+ self.log.debug('Checking %s restarted since %s' % (service, mtime))
2090+ time.sleep(sleep_time)
2091+ proc_start_time = self._get_proc_start_time(sentry_unit, service,
2092+ pgrep_full)
2093+ while retry_count > 0 and not proc_start_time:
2094+ self.log.debug('No pid file found for service %s, will retry %i '
2095+ 'more times' % (service, retry_count))
2096+ time.sleep(30)
2097+ proc_start_time = self._get_proc_start_time(sentry_unit, service,
2098+ pgrep_full)
2099+ retry_count = retry_count - 1
2100+
2101+ if not proc_start_time:
2102+ self.log.warn('No proc start time found, assuming service did '
2103+ 'not start')
2104+ return False
2105+ if proc_start_time >= mtime:
2106+ self.log.debug('proc start time is newer than provided mtime'
2107+ '(%s >= %s)' % (proc_start_time, mtime))
2108+ return True
2109+ else:
2110+ self.log.warn('proc start time (%s) is older than provided mtime '
2111+ '(%s), service did not restart' % (proc_start_time,
2112+ mtime))
2113+ return False
2114+
2115+ def config_updated_since(self, sentry_unit, filename, mtime,
2116+ sleep_time=20):
2117+ """Check if file was modified after a given time.
2118+
2119+ Args:
2120+ sentry_unit (sentry): The sentry unit to check the file mtime on
2121+ filename (string): The file to check mtime of
2122+ mtime (float): The epoch time to check against
2123+ sleep_time (int): Seconds to sleep before looking for process
2124+
2125+ Returns:
2126+ bool: True if file was modified more recently than mtime, False if
2127+ file was modified before mtime,
2128+ """
2129+ self.log.debug('Checking %s updated since %s' % (filename, mtime))
2130+ time.sleep(sleep_time)
2131+ file_mtime = self._get_file_mtime(sentry_unit, filename)
2132+ if file_mtime >= mtime:
2133+ self.log.debug('File mtime is newer than provided mtime '
2134+ '(%s >= %s)' % (file_mtime, mtime))
2135+ return True
2136+ else:
2137+ self.log.warn('File mtime %s is older than provided mtime %s'
2138+ % (file_mtime, mtime))
2139+ return False
2140+
2141+ def validate_service_config_changed(self, sentry_unit, mtime, service,
2142+ filename, pgrep_full=False,
2143+ sleep_time=20, retry_count=2):
2144+ """Check service and file were updated after mtime
2145+
2146+ Args:
2147+ sentry_unit (sentry): The sentry unit to check for the service on
2148+ mtime (float): The epoch time to check against
2149+ service (string): service name to look for in process table
2150+ filename (string): The file to check mtime of
2151+ pgrep_full (boolean): Use full command line search mode with pgrep
2152+ sleep_time (int): Seconds to sleep before looking for process
2153+ retry_count (int): If service is not found, how many times to retry
2154+
2155+ Typical Usage:
2156+ u = OpenStackAmuletUtils(ERROR)
2157+ ...
2158+ mtime = u.get_sentry_time(self.cinder_sentry)
2159+ self.d.configure('cinder', {'verbose': 'True', 'debug': 'True'})
2160+ if not u.validate_service_config_changed(self.cinder_sentry,
2161+ mtime,
2162+ 'cinder-api',
2163+ '/etc/cinder/cinder.conf')
2164+ amulet.raise_status(amulet.FAIL, msg='update failed')
2165+ Returns:
2166+ bool: True if both service and file where updated/restarted after
2167+ mtime, False if service is older than mtime or if service was
2168+ not found or if filename was modified before mtime.
2169+ """
2170+ self.log.debug('Checking %s restarted since %s' % (service, mtime))
2171+ time.sleep(sleep_time)
2172+ service_restart = self.service_restarted_since(sentry_unit, mtime,
2173+ service,
2174+ pgrep_full=pgrep_full,
2175+ sleep_time=0,
2176+ retry_count=retry_count)
2177+ config_update = self.config_updated_since(sentry_unit, filename, mtime,
2178+ sleep_time=0)
2179+ return service_restart and config_update
2180+
2181+ def get_sentry_time(self, sentry_unit):
2182+ """Return current epoch time on a sentry"""
2183+ cmd = "date +'%s'"
2184+ return float(sentry_unit.run(cmd)[0])
2185+
2186 def relation_error(self, name, data):
2187 return 'unexpected relation data in {} - {}'.format(name, data)
2188
2189
2190=== modified file 'tests/charmhelpers/contrib/openstack/amulet/deployment.py'
2191--- tests/charmhelpers/contrib/openstack/amulet/deployment.py 2015-01-29 13:02:55 +0000
2192+++ tests/charmhelpers/contrib/openstack/amulet/deployment.py 2015-04-16 21:56:47 +0000
2193@@ -15,6 +15,7 @@
2194 # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2195
2196 import six
2197+from collections import OrderedDict
2198 from charmhelpers.contrib.amulet.deployment import (
2199 AmuletDeployment
2200 )
2201@@ -43,7 +44,7 @@
2202 Determine if the local branch being tested is derived from its
2203 stable or next (dev) branch, and based on this, use the corresonding
2204 stable or next branches for the other_services."""
2205- base_charms = ['mysql', 'mongodb', 'rabbitmq-server']
2206+ base_charms = ['mysql', 'mongodb']
2207
2208 if self.stable:
2209 for svc in other_services:
2210@@ -71,16 +72,19 @@
2211 services.append(this_service)
2212 use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph',
2213 'ceph-osd', 'ceph-radosgw']
2214+ # Openstack subordinate charms do not expose an origin option as that
2215+ # is controlled by the principle
2216+ ignore = ['neutron-openvswitch']
2217
2218 if self.openstack:
2219 for svc in services:
2220- if svc['name'] not in use_source:
2221+ if svc['name'] not in use_source + ignore:
2222 config = {'openstack-origin': self.openstack}
2223 self.d.configure(svc['name'], config)
2224
2225 if self.source:
2226 for svc in services:
2227- if svc['name'] in use_source:
2228+ if svc['name'] in use_source and svc['name'] not in ignore:
2229 config = {'source': self.source}
2230 self.d.configure(svc['name'], config)
2231
2232@@ -97,12 +101,37 @@
2233 """
2234 (self.precise_essex, self.precise_folsom, self.precise_grizzly,
2235 self.precise_havana, self.precise_icehouse,
2236- self.trusty_icehouse) = range(6)
2237+ self.trusty_icehouse, self.trusty_juno, self.trusty_kilo,
2238+ self.utopic_juno, self.vivid_kilo) = range(10)
2239 releases = {
2240 ('precise', None): self.precise_essex,
2241 ('precise', 'cloud:precise-folsom'): self.precise_folsom,
2242 ('precise', 'cloud:precise-grizzly'): self.precise_grizzly,
2243 ('precise', 'cloud:precise-havana'): self.precise_havana,
2244 ('precise', 'cloud:precise-icehouse'): self.precise_icehouse,
2245- ('trusty', None): self.trusty_icehouse}
2246+ ('trusty', None): self.trusty_icehouse,
2247+ ('trusty', 'cloud:trusty-juno'): self.trusty_juno,
2248+ ('trusty', 'cloud:trusty-kilo'): self.trusty_kilo,
2249+ ('utopic', None): self.utopic_juno,
2250+ ('vivid', None): self.vivid_kilo}
2251 return releases[(self.series, self.openstack)]
2252+
2253+ def _get_openstack_release_string(self):
2254+ """Get openstack release string.
2255+
2256+ Return a string representing the openstack release.
2257+ """
2258+ releases = OrderedDict([
2259+ ('precise', 'essex'),
2260+ ('quantal', 'folsom'),
2261+ ('raring', 'grizzly'),
2262+ ('saucy', 'havana'),
2263+ ('trusty', 'icehouse'),
2264+ ('utopic', 'juno'),
2265+ ('vivid', 'kilo'),
2266+ ])
2267+ if self.openstack:
2268+ os_origin = self.openstack.split(':')[1]
2269+ return os_origin.split('%s-' % self.series)[1].split('/')[0]
2270+ else:
2271+ return releases[self.series]

Subscribers

People subscribed via source and target branches