Merge lp:~gnuoy/charms/trusty/openstack-dashboard/lp-1373714 into lp:~openstack-charmers-archive/charms/trusty/openstack-dashboard/next

Proposed by Liam Young
Status: Merged
Merged at revision: 39
Proposed branch: lp:~gnuoy/charms/trusty/openstack-dashboard/lp-1373714
Merge into: lp:~openstack-charmers-archive/charms/trusty/openstack-dashboard/next
Diff against target: 1355 lines (+679/-121)
17 files modified
config.yaml (+0/-8)
hooks/charmhelpers/contrib/hahelpers/apache.py (+10/-3)
hooks/charmhelpers/contrib/hahelpers/cluster.py (+1/-2)
hooks/charmhelpers/contrib/network/ip.py (+185/-16)
hooks/charmhelpers/contrib/openstack/amulet/deployment.py (+38/-8)
hooks/charmhelpers/contrib/openstack/amulet/utils.py (+5/-4)
hooks/charmhelpers/contrib/openstack/context.py (+112/-34)
hooks/charmhelpers/contrib/openstack/ip.py (+1/-1)
hooks/charmhelpers/contrib/openstack/utils.py (+27/-1)
hooks/charmhelpers/core/hookenv.py (+42/-16)
hooks/charmhelpers/core/host.py (+30/-5)
hooks/charmhelpers/core/services/base.py (+3/-0)
hooks/charmhelpers/core/services/helpers.py (+119/-5)
hooks/charmhelpers/fetch/__init__.py (+19/-5)
hooks/charmhelpers/fetch/archiveurl.py (+49/-4)
hooks/horizon_hooks.py (+23/-4)
unit_tests/test_horizon_hooks.py (+15/-5)
To merge this branch: bzr merge lp:~gnuoy/charms/trusty/openstack-dashboard/lp-1373714
Reviewer Review Type Date Requested Status
OpenStack Charmers Pending
Review via email: mp+236720@code.launchpad.net
To post a comment you must log in.

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 2014-06-10 13:45:26 +0000
3+++ config.yaml 2014-10-01 14:35:37 +0000
4@@ -35,14 +35,6 @@
5 vip:
6 type: string
7 description: "Virtual IP to use to front openstack dashboard ha configuration"
8- vip_iface:
9- type: string
10- default: eth0
11- description: "Network Interface where to place the Virtual IP"
12- vip_cidr:
13- type: int
14- default: 24
15- description: "Netmask that will be used for the Virtual IP"
16 ha-bindiface:
17 type: string
18 default: eth0
19
20=== modified file 'hooks/charmhelpers/contrib/hahelpers/apache.py'
21--- hooks/charmhelpers/contrib/hahelpers/apache.py 2014-03-27 11:15:06 +0000
22+++ hooks/charmhelpers/contrib/hahelpers/apache.py 2014-10-01 14:35:37 +0000
23@@ -20,20 +20,27 @@
24 )
25
26
27-def get_cert():
28+def get_cert(cn=None):
29+ # TODO: deal with multiple https endpoints via charm config
30 cert = config_get('ssl_cert')
31 key = config_get('ssl_key')
32 if not (cert and key):
33 log("Inspecting identity-service relations for SSL certificate.",
34 level=INFO)
35 cert = key = None
36+ if cn:
37+ ssl_cert_attr = 'ssl_cert_{}'.format(cn)
38+ ssl_key_attr = 'ssl_key_{}'.format(cn)
39+ else:
40+ ssl_cert_attr = 'ssl_cert'
41+ ssl_key_attr = 'ssl_key'
42 for r_id in relation_ids('identity-service'):
43 for unit in relation_list(r_id):
44 if not cert:
45- cert = relation_get('ssl_cert',
46+ cert = relation_get(ssl_cert_attr,
47 rid=r_id, unit=unit)
48 if not key:
49- key = relation_get('ssl_key',
50+ key = relation_get(ssl_key_attr,
51 rid=r_id, unit=unit)
52 return (cert, key)
53
54
55=== modified file 'hooks/charmhelpers/contrib/hahelpers/cluster.py'
56--- hooks/charmhelpers/contrib/hahelpers/cluster.py 2014-08-13 13:12:30 +0000
57+++ hooks/charmhelpers/contrib/hahelpers/cluster.py 2014-10-01 14:35:37 +0000
58@@ -139,10 +139,9 @@
59 return True
60 for r_id in relation_ids('identity-service'):
61 for unit in relation_list(r_id):
62+ # TODO - needs fixing for new helper as ssl_cert/key suffixes with CN
63 rel_state = [
64 relation_get('https_keystone', rid=r_id, unit=unit),
65- relation_get('ssl_cert', rid=r_id, unit=unit),
66- relation_get('ssl_key', rid=r_id, unit=unit),
67 relation_get('ca_cert', rid=r_id, unit=unit),
68 ]
69 # NOTE: works around (LP: #1203241)
70
71=== modified file 'hooks/charmhelpers/contrib/network/ip.py'
72--- hooks/charmhelpers/contrib/network/ip.py 2014-08-13 13:47:01 +0000
73+++ hooks/charmhelpers/contrib/network/ip.py 2014-10-01 14:35:37 +0000
74@@ -1,10 +1,16 @@
75+import glob
76+import re
77+import subprocess
78 import sys
79
80 from functools import partial
81
82+from charmhelpers.core.hookenv import unit_get
83 from charmhelpers.fetch import apt_install
84 from charmhelpers.core.hookenv import (
85- ERROR, log, config,
86+ WARNING,
87+ ERROR,
88+ log
89 )
90
91 try:
92@@ -156,19 +162,182 @@
93 get_netmask_for_address = partial(_get_for_address, key='netmask')
94
95
96-def get_ipv6_addr(iface="eth0"):
97+def format_ipv6_addr(address):
98+ """
99+ IPv6 needs to be wrapped with [] in url link to parse correctly.
100+ """
101+ if is_ipv6(address):
102+ address = "[%s]" % address
103+ else:
104+ log("Not a valid ipv6 address: %s" % address, level=WARNING)
105+ address = None
106+
107+ return address
108+
109+
110+def get_iface_addr(iface='eth0', inet_type='AF_INET', inc_aliases=False,
111+ fatal=True, exc_list=None):
112+ """
113+ Return the assigned IP address for a given interface, if any, or [].
114+ """
115+ # Extract nic if passed /dev/ethX
116+ if '/' in iface:
117+ iface = iface.split('/')[-1]
118+ if not exc_list:
119+ exc_list = []
120 try:
121- iface_addrs = netifaces.ifaddresses(iface)
122- if netifaces.AF_INET6 not in iface_addrs:
123- raise Exception("Interface '%s' doesn't have an ipv6 address." % iface)
124-
125- addresses = netifaces.ifaddresses(iface)[netifaces.AF_INET6]
126- ipv6_addr = [a['addr'] for a in addresses if not a['addr'].startswith('fe80')
127- and config('vip') != a['addr']]
128- if not ipv6_addr:
129- raise Exception("Interface '%s' doesn't have global ipv6 address." % iface)
130-
131- return ipv6_addr[0]
132-
133- except ValueError:
134- raise ValueError("Invalid interface '%s'" % iface)
135+ inet_num = getattr(netifaces, inet_type)
136+ except AttributeError:
137+ raise Exception('Unknown inet type ' + str(inet_type))
138+
139+ interfaces = netifaces.interfaces()
140+ if inc_aliases:
141+ ifaces = []
142+ for _iface in interfaces:
143+ if iface == _iface or _iface.split(':')[0] == iface:
144+ ifaces.append(_iface)
145+ if fatal and not ifaces:
146+ raise Exception("Invalid interface '%s'" % iface)
147+ ifaces.sort()
148+ else:
149+ if iface not in interfaces:
150+ if fatal:
151+ raise Exception("%s not found " % (iface))
152+ else:
153+ return []
154+ else:
155+ ifaces = [iface]
156+
157+ addresses = []
158+ for netiface in ifaces:
159+ net_info = netifaces.ifaddresses(netiface)
160+ if inet_num in net_info:
161+ for entry in net_info[inet_num]:
162+ if 'addr' in entry and entry['addr'] not in exc_list:
163+ addresses.append(entry['addr'])
164+ if fatal and not addresses:
165+ raise Exception("Interface '%s' doesn't have any %s addresses." %
166+ (iface, inet_type))
167+ return addresses
168+
169+get_ipv4_addr = partial(get_iface_addr, inet_type='AF_INET')
170+
171+
172+def get_iface_from_addr(addr):
173+ """Work out on which interface the provided address is configured."""
174+ for iface in netifaces.interfaces():
175+ addresses = netifaces.ifaddresses(iface)
176+ for inet_type in addresses:
177+ for _addr in addresses[inet_type]:
178+ _addr = _addr['addr']
179+ # link local
180+ ll_key = re.compile("(.+)%.*")
181+ raw = re.match(ll_key, _addr)
182+ if raw:
183+ _addr = raw.group(1)
184+ if _addr == addr:
185+ log("Address '%s' is configured on iface '%s'" %
186+ (addr, iface))
187+ return iface
188+
189+ msg = "Unable to infer net iface on which '%s' is configured" % (addr)
190+ raise Exception(msg)
191+
192+
193+def sniff_iface(f):
194+ """If no iface provided, inject net iface inferred from unit private
195+ address.
196+ """
197+ def iface_sniffer(*args, **kwargs):
198+ if not kwargs.get('iface', None):
199+ kwargs['iface'] = get_iface_from_addr(unit_get('private-address'))
200+
201+ return f(*args, **kwargs)
202+
203+ return iface_sniffer
204+
205+
206+@sniff_iface
207+def get_ipv6_addr(iface=None, inc_aliases=False, fatal=True, exc_list=None,
208+ dynamic_only=True):
209+ """Get assigned IPv6 address for a given interface.
210+
211+ Returns list of addresses found. If no address found, returns empty list.
212+
213+ If iface is None, we infer the current primary interface by doing a reverse
214+ lookup on the unit private-address.
215+
216+ We currently only support scope global IPv6 addresses i.e. non-temporary
217+ addresses. If no global IPv6 address is found, return the first one found
218+ in the ipv6 address list.
219+ """
220+ addresses = get_iface_addr(iface=iface, inet_type='AF_INET6',
221+ inc_aliases=inc_aliases, fatal=fatal,
222+ exc_list=exc_list)
223+
224+ if addresses:
225+ global_addrs = []
226+ for addr in addresses:
227+ key_scope_link_local = re.compile("^fe80::..(.+)%(.+)")
228+ m = re.match(key_scope_link_local, addr)
229+ if m:
230+ eui_64_mac = m.group(1)
231+ iface = m.group(2)
232+ else:
233+ global_addrs.append(addr)
234+
235+ if global_addrs:
236+ # Make sure any found global addresses are not temporary
237+ cmd = ['ip', 'addr', 'show', iface]
238+ out = subprocess.check_output(cmd)
239+ if dynamic_only:
240+ key = re.compile("inet6 (.+)/[0-9]+ scope global dynamic.*")
241+ else:
242+ key = re.compile("inet6 (.+)/[0-9]+ scope global.*")
243+
244+ addrs = []
245+ for line in out.split('\n'):
246+ line = line.strip()
247+ m = re.match(key, line)
248+ if m and 'temporary' not in line:
249+ # Return the first valid address we find
250+ for addr in global_addrs:
251+ if m.group(1) == addr:
252+ if not dynamic_only or \
253+ m.group(1).endswith(eui_64_mac):
254+ addrs.append(addr)
255+
256+ if addrs:
257+ return addrs
258+
259+ if fatal:
260+ raise Exception("Interface '%s' doesn't have a scope global "
261+ "non-temporary ipv6 address." % iface)
262+
263+ return []
264+
265+
266+def get_bridges(vnic_dir='/sys/devices/virtual/net'):
267+ """
268+ Return a list of bridges on the system or []
269+ """
270+ b_rgex = vnic_dir + '/*/bridge'
271+ return [x.replace(vnic_dir, '').split('/')[1] for x in glob.glob(b_rgex)]
272+
273+
274+def get_bridge_nics(bridge, vnic_dir='/sys/devices/virtual/net'):
275+ """
276+ Return a list of nics comprising a given bridge on the system or []
277+ """
278+ brif_rgex = "%s/%s/brif/*" % (vnic_dir, bridge)
279+ return [x.split('/')[-1] for x in glob.glob(brif_rgex)]
280+
281+
282+def is_bridge_member(nic):
283+ """
284+ Check if a given nic is a member of a bridge
285+ """
286+ for bridge in get_bridges():
287+ if nic in get_bridge_nics(bridge):
288+ return True
289+ return False
290
291=== modified file 'hooks/charmhelpers/contrib/openstack/amulet/deployment.py'
292--- hooks/charmhelpers/contrib/openstack/amulet/deployment.py 2014-08-13 13:12:30 +0000
293+++ hooks/charmhelpers/contrib/openstack/amulet/deployment.py 2014-10-01 14:35:37 +0000
294@@ -10,32 +10,62 @@
295 that is specifically for use by OpenStack charms.
296 """
297
298- def __init__(self, series=None, openstack=None, source=None):
299+ def __init__(self, series=None, openstack=None, source=None, stable=True):
300 """Initialize the deployment environment."""
301 super(OpenStackAmuletDeployment, self).__init__(series)
302 self.openstack = openstack
303 self.source = source
304+ self.stable = stable
305+ # Note(coreycb): this needs to be changed when new next branches come
306+ # out.
307+ self.current_next = "trusty"
308+
309+ def _determine_branch_locations(self, other_services):
310+ """Determine the branch locations for the other services.
311+
312+ Determine if the local branch being tested is derived from its
313+ stable or next (dev) branch, and based on this, use the corresonding
314+ stable or next branches for the other_services."""
315+ base_charms = ['mysql', 'mongodb', 'rabbitmq-server']
316+
317+ if self.stable:
318+ for svc in other_services:
319+ temp = 'lp:charms/{}'
320+ svc['location'] = temp.format(svc['name'])
321+ else:
322+ for svc in other_services:
323+ if svc['name'] in base_charms:
324+ temp = 'lp:charms/{}'
325+ svc['location'] = temp.format(svc['name'])
326+ else:
327+ temp = 'lp:~openstack-charmers/charms/{}/{}/next'
328+ svc['location'] = temp.format(self.current_next,
329+ svc['name'])
330+ return other_services
331
332 def _add_services(self, this_service, other_services):
333- """Add services to the deployment and set openstack-origin."""
334+ """Add services to the deployment and set openstack-origin/source."""
335+ other_services = self._determine_branch_locations(other_services)
336+
337 super(OpenStackAmuletDeployment, self)._add_services(this_service,
338 other_services)
339- name = 0
340+
341 services = other_services
342 services.append(this_service)
343- use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph']
344+ use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph',
345+ 'ceph-osd', 'ceph-radosgw']
346
347 if self.openstack:
348 for svc in services:
349- if svc[name] not in use_source:
350+ if svc['name'] not in use_source:
351 config = {'openstack-origin': self.openstack}
352- self.d.configure(svc[name], config)
353+ self.d.configure(svc['name'], config)
354
355 if self.source:
356 for svc in services:
357- if svc[name] in use_source:
358+ if svc['name'] in use_source:
359 config = {'source': self.source}
360- self.d.configure(svc[name], config)
361+ self.d.configure(svc['name'], config)
362
363 def _configure_services(self, configs):
364 """Configure all of the services."""
365
366=== modified file 'hooks/charmhelpers/contrib/openstack/amulet/utils.py'
367--- hooks/charmhelpers/contrib/openstack/amulet/utils.py 2014-08-13 13:12:30 +0000
368+++ hooks/charmhelpers/contrib/openstack/amulet/utils.py 2014-10-01 14:35:37 +0000
369@@ -187,15 +187,16 @@
370
371 f = opener.open("http://download.cirros-cloud.net/version/released")
372 version = f.read().strip()
373- cirros_img = "tests/cirros-{}-x86_64-disk.img".format(version)
374+ cirros_img = "cirros-{}-x86_64-disk.img".format(version)
375+ local_path = os.path.join('tests', cirros_img)
376
377- if not os.path.exists(cirros_img):
378+ if not os.path.exists(local_path):
379 cirros_url = "http://{}/{}/{}".format("download.cirros-cloud.net",
380 version, cirros_img)
381- opener.retrieve(cirros_url, cirros_img)
382+ opener.retrieve(cirros_url, local_path)
383 f.close()
384
385- with open(cirros_img) as f:
386+ with open(local_path) as f:
387 image = glance.images.create(name=image_name, is_public=True,
388 disk_format='qcow2',
389 container_format='bare', data=f)
390
391=== modified file 'hooks/charmhelpers/contrib/openstack/context.py'
392--- hooks/charmhelpers/contrib/openstack/context.py 2014-09-12 10:55:03 +0000
393+++ hooks/charmhelpers/contrib/openstack/context.py 2014-10-01 14:35:37 +0000
394@@ -8,7 +8,6 @@
395 check_call
396 )
397
398-
399 from charmhelpers.fetch import (
400 apt_install,
401 filter_installed_packages,
402@@ -28,6 +27,11 @@
403 INFO
404 )
405
406+from charmhelpers.core.host import (
407+ mkdir,
408+ write_file
409+)
410+
411 from charmhelpers.contrib.hahelpers.cluster import (
412 determine_apache_port,
413 determine_api_port,
414@@ -38,6 +42,7 @@
415 from charmhelpers.contrib.hahelpers.apache import (
416 get_cert,
417 get_ca_cert,
418+ install_ca_cert,
419 )
420
421 from charmhelpers.contrib.openstack.neutron import (
422@@ -47,6 +52,8 @@
423 from charmhelpers.contrib.network.ip import (
424 get_address_in_network,
425 get_ipv6_addr,
426+ format_ipv6_addr,
427+ is_address_in_network
428 )
429
430 CA_CERT_PATH = '/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt'
431@@ -168,8 +175,10 @@
432 for rid in relation_ids('shared-db'):
433 for unit in related_units(rid):
434 rdata = relation_get(rid=rid, unit=unit)
435+ host = rdata.get('db_host')
436+ host = format_ipv6_addr(host) or host
437 ctxt = {
438- 'database_host': rdata.get('db_host'),
439+ 'database_host': host,
440 'database': self.database,
441 'database_user': self.user,
442 'database_password': rdata.get(password_setting),
443@@ -245,10 +254,15 @@
444 for rid in relation_ids('identity-service'):
445 for unit in related_units(rid):
446 rdata = relation_get(rid=rid, unit=unit)
447+ serv_host = rdata.get('service_host')
448+ serv_host = format_ipv6_addr(serv_host) or serv_host
449+ auth_host = rdata.get('auth_host')
450+ auth_host = format_ipv6_addr(auth_host) or auth_host
451+
452 ctxt = {
453 'service_port': rdata.get('service_port'),
454- 'service_host': rdata.get('service_host'),
455- 'auth_host': rdata.get('auth_host'),
456+ 'service_host': serv_host,
457+ 'auth_host': auth_host,
458 'auth_port': rdata.get('auth_port'),
459 'admin_tenant_name': rdata.get('service_tenant'),
460 'admin_user': rdata.get('service_username'),
461@@ -297,11 +311,13 @@
462 for unit in related_units(rid):
463 if relation_get('clustered', rid=rid, unit=unit):
464 ctxt['clustered'] = True
465- ctxt['rabbitmq_host'] = relation_get('vip', rid=rid,
466- unit=unit)
467+ vip = relation_get('vip', rid=rid, unit=unit)
468+ vip = format_ipv6_addr(vip) or vip
469+ ctxt['rabbitmq_host'] = vip
470 else:
471- ctxt['rabbitmq_host'] = relation_get('private-address',
472- rid=rid, unit=unit)
473+ host = relation_get('private-address', rid=rid, unit=unit)
474+ host = format_ipv6_addr(host) or host
475+ ctxt['rabbitmq_host'] = host
476 ctxt.update({
477 'rabbitmq_user': username,
478 'rabbitmq_password': relation_get('password', rid=rid,
479@@ -340,8 +356,9 @@
480 and len(related_units(rid)) > 1:
481 rabbitmq_hosts = []
482 for unit in related_units(rid):
483- rabbitmq_hosts.append(relation_get('private-address',
484- rid=rid, unit=unit))
485+ host = relation_get('private-address', rid=rid, unit=unit)
486+ host = format_ipv6_addr(host) or host
487+ rabbitmq_hosts.append(host)
488 ctxt['rabbitmq_hosts'] = ','.join(rabbitmq_hosts)
489 if not context_complete(ctxt):
490 return {}
491@@ -370,6 +387,7 @@
492 ceph_addr = \
493 relation_get('ceph-public-address', rid=rid, unit=unit) or \
494 relation_get('private-address', rid=rid, unit=unit)
495+ ceph_addr = format_ipv6_addr(ceph_addr) or ceph_addr
496 mon_hosts.append(ceph_addr)
497
498 ctxt = {
499@@ -404,10 +422,12 @@
500
501 cluster_hosts = {}
502 l_unit = local_unit().replace('/', '-')
503+
504 if config('prefer-ipv6'):
505- addr = get_ipv6_addr()
506+ addr = get_ipv6_addr(exc_list=[config('vip')])[0]
507 else:
508 addr = unit_get('private-address')
509+
510 cluster_hosts[l_unit] = get_address_in_network(config('os-internal-network'),
511 addr)
512
513@@ -421,6 +441,11 @@
514 'units': cluster_hosts,
515 }
516
517+ if config('haproxy-server-timeout'):
518+ ctxt['haproxy_server_timeout'] = config('haproxy-server-timeout')
519+ if config('haproxy-client-timeout'):
520+ ctxt['haproxy_client_timeout'] = config('haproxy-client-timeout')
521+
522 if config('prefer-ipv6'):
523 ctxt['local_host'] = 'ip6-localhost'
524 ctxt['haproxy_host'] = '::'
525@@ -490,22 +515,36 @@
526 cmd = ['a2enmod', 'ssl', 'proxy', 'proxy_http']
527 check_call(cmd)
528
529- def configure_cert(self):
530- if not os.path.isdir('/etc/apache2/ssl'):
531- os.mkdir('/etc/apache2/ssl')
532+ def configure_cert(self, cn=None):
533 ssl_dir = os.path.join('/etc/apache2/ssl/', self.service_namespace)
534- if not os.path.isdir(ssl_dir):
535- os.mkdir(ssl_dir)
536- cert, key = get_cert()
537- with open(os.path.join(ssl_dir, 'cert'), 'w') as cert_out:
538- cert_out.write(b64decode(cert))
539- with open(os.path.join(ssl_dir, 'key'), 'w') as key_out:
540- key_out.write(b64decode(key))
541+ mkdir(path=ssl_dir)
542+ cert, key = get_cert(cn)
543+ if cn:
544+ cert_filename = 'cert_{}'.format(cn)
545+ key_filename = 'key_{}'.format(cn)
546+ else:
547+ cert_filename = 'cert'
548+ key_filename = 'key'
549+ write_file(path=os.path.join(ssl_dir, cert_filename),
550+ content=b64decode(cert))
551+ write_file(path=os.path.join(ssl_dir, key_filename),
552+ content=b64decode(key))
553+
554+ def configure_ca(self):
555 ca_cert = get_ca_cert()
556 if ca_cert:
557- with open(CA_CERT_PATH, 'w') as ca_out:
558- ca_out.write(b64decode(ca_cert))
559- check_call(['update-ca-certificates'])
560+ install_ca_cert(b64decode(ca_cert))
561+
562+ def canonical_names(self):
563+ '''Figure out which canonical names clients will access this service'''
564+ cns = []
565+ for r_id in relation_ids('identity-service'):
566+ for unit in related_units(r_id):
567+ rdata = relation_get(rid=r_id, unit=unit)
568+ for k in rdata:
569+ if k.startswith('ssl_key_'):
570+ cns.append(k.lstrip('ssl_key_'))
571+ return list(set(cns))
572
573 def __call__(self):
574 if isinstance(self.external_ports, basestring):
575@@ -513,21 +552,47 @@
576 if (not self.external_ports or not https()):
577 return {}
578
579- self.configure_cert()
580+ self.configure_ca()
581 self.enable_modules()
582
583 ctxt = {
584 'namespace': self.service_namespace,
585- 'private_address': unit_get('private-address'),
586- 'endpoints': []
587+ 'endpoints': [],
588+ 'ext_ports': []
589 }
590- if is_clustered():
591- ctxt['private_address'] = config('vip')
592- for api_port in self.external_ports:
593- ext_port = determine_apache_port(api_port)
594- int_port = determine_api_port(api_port)
595- portmap = (int(ext_port), int(int_port))
596- ctxt['endpoints'].append(portmap)
597+
598+ for cn in self.canonical_names():
599+ self.configure_cert(cn)
600+
601+ addresses = []
602+ vips = []
603+ if config('vip'):
604+ vips = config('vip').split()
605+
606+ for network_type in ['os-internal-network',
607+ 'os-admin-network',
608+ 'os-public-network']:
609+ address = get_address_in_network(config(network_type),
610+ unit_get('private-address'))
611+ if len(vips) > 0 and is_clustered():
612+ for vip in vips:
613+ if is_address_in_network(config(network_type),
614+ vip):
615+ addresses.append((address, vip))
616+ break
617+ elif is_clustered():
618+ addresses.append((address, config('vip')))
619+ else:
620+ addresses.append((address, address))
621+
622+ for address, endpoint in set(addresses):
623+ for api_port in self.external_ports:
624+ ext_port = determine_apache_port(api_port)
625+ int_port = determine_api_port(api_port)
626+ portmap = (address, endpoint, int(ext_port), int(int_port))
627+ ctxt['endpoints'].append(portmap)
628+ ctxt['ext_ports'].append(int(ext_port))
629+ ctxt['ext_ports'] = list(set(ctxt['ext_ports']))
630 return ctxt
631
632
633@@ -787,3 +852,16 @@
634 'use_syslog': config('use-syslog')
635 }
636 return ctxt
637+
638+
639+class BindHostContext(OSContextGenerator):
640+
641+ def __call__(self):
642+ if config('prefer-ipv6'):
643+ return {
644+ 'bind_host': '::'
645+ }
646+ else:
647+ return {
648+ 'bind_host': '0.0.0.0'
649+ }
650
651=== modified file 'hooks/charmhelpers/contrib/openstack/ip.py'
652--- hooks/charmhelpers/contrib/openstack/ip.py 2014-08-13 13:12:30 +0000
653+++ hooks/charmhelpers/contrib/openstack/ip.py 2014-10-01 14:35:37 +0000
654@@ -66,7 +66,7 @@
655 resolved_address = vip
656 else:
657 if config('prefer-ipv6'):
658- fallback_addr = get_ipv6_addr()
659+ fallback_addr = get_ipv6_addr(exc_list=[config('vip')])[0]
660 else:
661 fallback_addr = unit_get(_address_map[endpoint_type]['fallback'])
662 resolved_address = get_address_in_network(
663
664=== modified file 'hooks/charmhelpers/contrib/openstack/utils.py'
665--- hooks/charmhelpers/contrib/openstack/utils.py 2014-09-12 10:55:03 +0000
666+++ hooks/charmhelpers/contrib/openstack/utils.py 2014-10-01 14:35:37 +0000
667@@ -4,6 +4,7 @@
668 from collections import OrderedDict
669
670 import subprocess
671+import json
672 import os
673 import socket
674 import sys
675@@ -13,7 +14,9 @@
676 log as juju_log,
677 charm_dir,
678 ERROR,
679- INFO
680+ INFO,
681+ relation_ids,
682+ relation_set
683 )
684
685 from charmhelpers.contrib.storage.linux.lvm import (
686@@ -22,6 +25,10 @@
687 remove_lvm_physical_volume,
688 )
689
690+from charmhelpers.contrib.network.ip import (
691+ get_ipv6_addr
692+)
693+
694 from charmhelpers.core.host import lsb_release, mounts, umount
695 from charmhelpers.fetch import apt_install, apt_cache
696 from charmhelpers.contrib.storage.linux.utils import is_block_device, zap_disk
697@@ -70,6 +77,7 @@
698 ('1.13.0', 'icehouse'),
699 ('1.12.0', 'icehouse'),
700 ('1.11.0', 'icehouse'),
701+ ('2.0.0', 'juno'),
702 ])
703
704 DEFAULT_LOOPBACK_SIZE = '5G'
705@@ -456,3 +464,21 @@
706 return result
707 else:
708 return result.split('.')[0]
709+
710+
711+def sync_db_with_multi_ipv6_addresses(database, database_user,
712+ relation_prefix=None):
713+ hosts = get_ipv6_addr(dynamic_only=False)
714+
715+ kwargs = {'database': database,
716+ 'username': database_user,
717+ 'hostname': json.dumps(hosts)}
718+
719+ if relation_prefix:
720+ keys = kwargs.keys()
721+ for key in keys:
722+ kwargs["%s_%s" % (relation_prefix, key)] = kwargs[key]
723+ del kwargs[key]
724+
725+ for rid in relation_ids('shared-db'):
726+ relation_set(relation_id=rid, **kwargs)
727
728=== modified file 'hooks/charmhelpers/core/hookenv.py'
729--- hooks/charmhelpers/core/hookenv.py 2014-08-26 13:29:06 +0000
730+++ hooks/charmhelpers/core/hookenv.py 2014-10-01 14:35:37 +0000
731@@ -156,12 +156,15 @@
732
733
734 class Config(dict):
735- """A Juju charm config dictionary that can write itself to
736- disk (as json) and track which values have changed since
737- the previous hook invocation.
738-
739- Do not instantiate this object directly - instead call
740- ``hookenv.config()``
741+ """A dictionary representation of the charm's config.yaml, with some
742+ extra features:
743+
744+ - See which values in the dictionary have changed since the previous hook.
745+ - For values that have changed, see what the previous value was.
746+ - Store arbitrary data for use in a later hook.
747+
748+ NOTE: Do not instantiate this object directly - instead call
749+ ``hookenv.config()``, which will return an instance of :class:`Config`.
750
751 Example usage::
752
753@@ -170,8 +173,8 @@
754 >>> config = hookenv.config()
755 >>> config['foo']
756 'bar'
757+ >>> # store a new key/value for later use
758 >>> config['mykey'] = 'myval'
759- >>> config.save()
760
761
762 >>> # user runs `juju set mycharm foo=baz`
763@@ -188,22 +191,34 @@
764 >>> # keys/values that we add are preserved across hooks
765 >>> config['mykey']
766 'myval'
767- >>> # don't forget to save at the end of hook!
768- >>> config.save()
769
770 """
771 CONFIG_FILE_NAME = '.juju-persistent-config'
772
773 def __init__(self, *args, **kw):
774 super(Config, self).__init__(*args, **kw)
775+ self.implicit_save = True
776 self._prev_dict = None
777 self.path = os.path.join(charm_dir(), Config.CONFIG_FILE_NAME)
778 if os.path.exists(self.path):
779 self.load_previous()
780
781+ def __getitem__(self, key):
782+ """For regular dict lookups, check the current juju config first,
783+ then the previous (saved) copy. This ensures that user-saved values
784+ will be returned by a dict lookup.
785+
786+ """
787+ try:
788+ return dict.__getitem__(self, key)
789+ except KeyError:
790+ return (self._prev_dict or {})[key]
791+
792 def load_previous(self, path=None):
793- """Load previous copy of config from disk so that current values
794- can be compared to previous values.
795+ """Load previous copy of config from disk.
796+
797+ In normal usage you don't need to call this method directly - it
798+ is called automatically at object initialization.
799
800 :param path:
801
802@@ -218,8 +233,8 @@
803 self._prev_dict = json.load(f)
804
805 def changed(self, key):
806- """Return true if the value for this key has changed since
807- the last save.
808+ """Return True if the current value for this key is different from
809+ the previous value.
810
811 """
812 if self._prev_dict is None:
813@@ -228,7 +243,7 @@
814
815 def previous(self, key):
816 """Return previous value for this key, or None if there
817- is no "previous" value.
818+ is no previous value.
819
820 """
821 if self._prev_dict:
822@@ -238,7 +253,13 @@
823 def save(self):
824 """Save this config to disk.
825
826- Preserves items in _prev_dict that do not exist in self.
827+ If the charm is using the :mod:`Services Framework <services.base>`
828+ or :meth:'@hook <Hooks.hook>' decorator, this
829+ is called automatically at the end of successful hook execution.
830+ Otherwise, it should be called directly by user code.
831+
832+ To disable automatic saves, set ``implicit_save=False`` on this
833+ instance.
834
835 """
836 if self._prev_dict:
837@@ -465,9 +486,10 @@
838 hooks.execute(sys.argv)
839 """
840
841- def __init__(self):
842+ def __init__(self, config_save=True):
843 super(Hooks, self).__init__()
844 self._hooks = {}
845+ self._config_save = config_save
846
847 def register(self, name, function):
848 """Register a hook"""
849@@ -478,6 +500,10 @@
850 hook_name = os.path.basename(args[0])
851 if hook_name in self._hooks:
852 self._hooks[hook_name]()
853+ if self._config_save:
854+ cfg = config()
855+ if cfg.implicit_save:
856+ cfg.save()
857 else:
858 raise UnregisteredHookError(hook_name)
859
860
861=== modified file 'hooks/charmhelpers/core/host.py'
862--- hooks/charmhelpers/core/host.py 2014-09-12 10:55:03 +0000
863+++ hooks/charmhelpers/core/host.py 2014-10-01 14:35:37 +0000
864@@ -68,8 +68,8 @@
865 """Determine whether a system service is available"""
866 try:
867 subprocess.check_output(['service', service_name, 'status'], stderr=subprocess.STDOUT)
868- except subprocess.CalledProcessError:
869- return False
870+ except subprocess.CalledProcessError as e:
871+ return 'unrecognized service' not in e.output
872 else:
873 return True
874
875@@ -209,10 +209,15 @@
876 return system_mounts
877
878
879-def file_hash(path):
880- """Generate a md5 hash of the contents of 'path' or None if not found """
881+def file_hash(path, hash_type='md5'):
882+ """
883+ Generate a hash checksum of the contents of 'path' or None if not found.
884+
885+ :param str hash_type: Any hash alrgorithm supported by :mod:`hashlib`,
886+ such as md5, sha1, sha256, sha512, etc.
887+ """
888 if os.path.exists(path):
889- h = hashlib.md5()
890+ h = getattr(hashlib, hash_type)()
891 with open(path, 'r') as source:
892 h.update(source.read()) # IGNORE:E1101 - it does have update
893 return h.hexdigest()
894@@ -220,6 +225,26 @@
895 return None
896
897
898+def check_hash(path, checksum, hash_type='md5'):
899+ """
900+ Validate a file using a cryptographic checksum.
901+
902+ :param str checksum: Value of the checksum used to validate the file.
903+ :param str hash_type: Hash algorithm used to generate `checksum`.
904+ Can be any hash alrgorithm supported by :mod:`hashlib`,
905+ such as md5, sha1, sha256, sha512, etc.
906+ :raises ChecksumError: If the file fails the checksum
907+
908+ """
909+ actual_checksum = file_hash(path, hash_type)
910+ if checksum != actual_checksum:
911+ raise ChecksumError("'%s' != '%s'" % (checksum, actual_checksum))
912+
913+
914+class ChecksumError(ValueError):
915+ pass
916+
917+
918 def restart_on_change(restart_map, stopstart=False):
919 """Restart services based on configuration files changing
920
921
922=== modified file 'hooks/charmhelpers/core/services/base.py'
923--- hooks/charmhelpers/core/services/base.py 2014-08-26 13:29:06 +0000
924+++ hooks/charmhelpers/core/services/base.py 2014-10-01 14:35:37 +0000
925@@ -118,6 +118,9 @@
926 else:
927 self.provide_data()
928 self.reconfigure_services()
929+ cfg = hookenv.config()
930+ if cfg.implicit_save:
931+ cfg.save()
932
933 def provide_data(self):
934 """
935
936=== modified file 'hooks/charmhelpers/core/services/helpers.py'
937--- hooks/charmhelpers/core/services/helpers.py 2014-08-13 13:12:30 +0000
938+++ hooks/charmhelpers/core/services/helpers.py 2014-10-01 14:35:37 +0000
939@@ -1,3 +1,5 @@
940+import os
941+import yaml
942 from charmhelpers.core import hookenv
943 from charmhelpers.core import templating
944
945@@ -19,15 +21,21 @@
946 the `name` attribute that are complete will used to populate the dictionary
947 values (see `get_data`, below).
948
949- The generated context will be namespaced under the interface type, to prevent
950- potential naming conflicts.
951+ The generated context will be namespaced under the relation :attr:`name`,
952+ to prevent potential naming conflicts.
953+
954+ :param str name: Override the relation :attr:`name`, since it can vary from charm to charm
955+ :param list additional_required_keys: Extend the list of :attr:`required_keys`
956 """
957 name = None
958 interface = None
959 required_keys = []
960
961- def __init__(self, *args, **kwargs):
962- super(RelationContext, self).__init__(*args, **kwargs)
963+ def __init__(self, name=None, additional_required_keys=None):
964+ if name is not None:
965+ self.name = name
966+ if additional_required_keys is not None:
967+ self.required_keys.extend(additional_required_keys)
968 self.get_data()
969
970 def __bool__(self):
971@@ -101,9 +109,115 @@
972 return {}
973
974
975+class MysqlRelation(RelationContext):
976+ """
977+ Relation context for the `mysql` interface.
978+
979+ :param str name: Override the relation :attr:`name`, since it can vary from charm to charm
980+ :param list additional_required_keys: Extend the list of :attr:`required_keys`
981+ """
982+ name = 'db'
983+ interface = 'mysql'
984+ required_keys = ['host', 'user', 'password', 'database']
985+
986+
987+class HttpRelation(RelationContext):
988+ """
989+ Relation context for the `http` interface.
990+
991+ :param str name: Override the relation :attr:`name`, since it can vary from charm to charm
992+ :param list additional_required_keys: Extend the list of :attr:`required_keys`
993+ """
994+ name = 'website'
995+ interface = 'http'
996+ required_keys = ['host', 'port']
997+
998+ def provide_data(self):
999+ return {
1000+ 'host': hookenv.unit_get('private-address'),
1001+ 'port': 80,
1002+ }
1003+
1004+
1005+class RequiredConfig(dict):
1006+ """
1007+ Data context that loads config options with one or more mandatory options.
1008+
1009+ Once the required options have been changed from their default values, all
1010+ config options will be available, namespaced under `config` to prevent
1011+ potential naming conflicts (for example, between a config option and a
1012+ relation property).
1013+
1014+ :param list *args: List of options that must be changed from their default values.
1015+ """
1016+
1017+ def __init__(self, *args):
1018+ self.required_options = args
1019+ self['config'] = hookenv.config()
1020+ with open(os.path.join(hookenv.charm_dir(), 'config.yaml')) as fp:
1021+ self.config = yaml.load(fp).get('options', {})
1022+
1023+ def __bool__(self):
1024+ for option in self.required_options:
1025+ if option not in self['config']:
1026+ return False
1027+ current_value = self['config'][option]
1028+ default_value = self.config[option].get('default')
1029+ if current_value == default_value:
1030+ return False
1031+ if current_value in (None, '') and default_value in (None, ''):
1032+ return False
1033+ return True
1034+
1035+ def __nonzero__(self):
1036+ return self.__bool__()
1037+
1038+
1039+class StoredContext(dict):
1040+ """
1041+ A data context that always returns the data that it was first created with.
1042+
1043+ This is useful to do a one-time generation of things like passwords, that
1044+ will thereafter use the same value that was originally generated, instead
1045+ of generating a new value each time it is run.
1046+ """
1047+ def __init__(self, file_name, config_data):
1048+ """
1049+ If the file exists, populate `self` with the data from the file.
1050+ Otherwise, populate with the given data and persist it to the file.
1051+ """
1052+ if os.path.exists(file_name):
1053+ self.update(self.read_context(file_name))
1054+ else:
1055+ self.store_context(file_name, config_data)
1056+ self.update(config_data)
1057+
1058+ def store_context(self, file_name, config_data):
1059+ if not os.path.isabs(file_name):
1060+ file_name = os.path.join(hookenv.charm_dir(), file_name)
1061+ with open(file_name, 'w') as file_stream:
1062+ os.fchmod(file_stream.fileno(), 0600)
1063+ yaml.dump(config_data, file_stream)
1064+
1065+ def read_context(self, file_name):
1066+ if not os.path.isabs(file_name):
1067+ file_name = os.path.join(hookenv.charm_dir(), file_name)
1068+ with open(file_name, 'r') as file_stream:
1069+ data = yaml.load(file_stream)
1070+ if not data:
1071+ raise OSError("%s is empty" % file_name)
1072+ return data
1073+
1074+
1075 class TemplateCallback(ManagerCallback):
1076 """
1077- Callback class that will render a template, for use as a ready action.
1078+ Callback class that will render a Jinja2 template, for use as a ready action.
1079+
1080+ :param str source: The template source file, relative to `$CHARM_DIR/templates`
1081+ :param str target: The target to write the rendered template to
1082+ :param str owner: The owner of the rendered file
1083+ :param str group: The group of the rendered file
1084+ :param int perms: The permissions of the rendered file
1085 """
1086 def __init__(self, source, target, owner='root', group='root', perms=0444):
1087 self.source = source
1088
1089=== modified file 'hooks/charmhelpers/fetch/__init__.py'
1090--- hooks/charmhelpers/fetch/__init__.py 2014-09-12 10:55:03 +0000
1091+++ hooks/charmhelpers/fetch/__init__.py 2014-10-01 14:35:37 +0000
1092@@ -208,7 +208,8 @@
1093 """Add a package source to this system.
1094
1095 @param source: a URL or sources.list entry, as supported by
1096- add-apt-repository(1). Examples:
1097+ add-apt-repository(1). Examples::
1098+
1099 ppa:charmers/example
1100 deb https://stub:key@private.example.com/ubuntu trusty main
1101
1102@@ -311,22 +312,35 @@
1103 apt_update(fatal=True)
1104
1105
1106-def install_remote(source):
1107+def install_remote(source, *args, **kwargs):
1108 """
1109 Install a file tree from a remote source
1110
1111 The specified source should be a url of the form:
1112 scheme://[host]/path[#[option=value][&...]]
1113
1114- Schemes supported are based on this modules submodules
1115- Options supported are submodule-specific"""
1116+ Schemes supported are based on this modules submodules.
1117+ Options supported are submodule-specific.
1118+ Additional arguments are passed through to the submodule.
1119+
1120+ For example::
1121+
1122+ dest = install_remote('http://example.com/archive.tgz',
1123+ checksum='deadbeef',
1124+ hash_type='sha1')
1125+
1126+ This will download `archive.tgz`, validate it using SHA1 and, if
1127+ the file is ok, extract it and return the directory in which it
1128+ was extracted. If the checksum fails, it will raise
1129+ :class:`charmhelpers.core.host.ChecksumError`.
1130+ """
1131 # We ONLY check for True here because can_handle may return a string
1132 # explaining why it can't handle a given source.
1133 handlers = [h for h in plugins() if h.can_handle(source) is True]
1134 installed_to = None
1135 for handler in handlers:
1136 try:
1137- installed_to = handler.install(source)
1138+ installed_to = handler.install(source, *args, **kwargs)
1139 except UnhandledSource:
1140 pass
1141 if not installed_to:
1142
1143=== modified file 'hooks/charmhelpers/fetch/archiveurl.py'
1144--- hooks/charmhelpers/fetch/archiveurl.py 2014-03-20 13:47:29 +0000
1145+++ hooks/charmhelpers/fetch/archiveurl.py 2014-10-01 14:35:37 +0000
1146@@ -1,6 +1,8 @@
1147 import os
1148 import urllib2
1149+from urllib import urlretrieve
1150 import urlparse
1151+import hashlib
1152
1153 from charmhelpers.fetch import (
1154 BaseFetchHandler,
1155@@ -10,11 +12,19 @@
1156 get_archive_handler,
1157 extract,
1158 )
1159-from charmhelpers.core.host import mkdir
1160+from charmhelpers.core.host import mkdir, check_hash
1161
1162
1163 class ArchiveUrlFetchHandler(BaseFetchHandler):
1164- """Handler for archives via generic URLs"""
1165+ """
1166+ Handler to download archive files from arbitrary URLs.
1167+
1168+ Can fetch from http, https, ftp, and file URLs.
1169+
1170+ Can install either tarballs (.tar, .tgz, .tbz2, etc) or zip files.
1171+
1172+ Installs the contents of the archive in $CHARM_DIR/fetched/.
1173+ """
1174 def can_handle(self, source):
1175 url_parts = self.parse_url(source)
1176 if url_parts.scheme not in ('http', 'https', 'ftp', 'file'):
1177@@ -24,6 +34,12 @@
1178 return False
1179
1180 def download(self, source, dest):
1181+ """
1182+ Download an archive file.
1183+
1184+ :param str source: URL pointing to an archive file.
1185+ :param str dest: Local path location to download archive file to.
1186+ """
1187 # propogate all exceptions
1188 # URLError, OSError, etc
1189 proto, netloc, path, params, query, fragment = urlparse.urlparse(source)
1190@@ -48,7 +64,30 @@
1191 os.unlink(dest)
1192 raise e
1193
1194- def install(self, source):
1195+ # Mandatory file validation via Sha1 or MD5 hashing.
1196+ def download_and_validate(self, url, hashsum, validate="sha1"):
1197+ tempfile, headers = urlretrieve(url)
1198+ check_hash(tempfile, hashsum, validate)
1199+ return tempfile
1200+
1201+ def install(self, source, dest=None, checksum=None, hash_type='sha1'):
1202+ """
1203+ Download and install an archive file, with optional checksum validation.
1204+
1205+ The checksum can also be given on the `source` URL's fragment.
1206+ For example::
1207+
1208+ handler.install('http://example.com/file.tgz#sha1=deadbeef')
1209+
1210+ :param str source: URL pointing to an archive file.
1211+ :param str dest: Local destination path to install to. If not given,
1212+ installs to `$CHARM_DIR/archives/archive_file_name`.
1213+ :param str checksum: If given, validate the archive file after download.
1214+ :param str hash_type: Algorithm used to generate `checksum`.
1215+ Can be any hash alrgorithm supported by :mod:`hashlib`,
1216+ such as md5, sha1, sha256, sha512, etc.
1217+
1218+ """
1219 url_parts = self.parse_url(source)
1220 dest_dir = os.path.join(os.environ.get('CHARM_DIR'), 'fetched')
1221 if not os.path.exists(dest_dir):
1222@@ -60,4 +99,10 @@
1223 raise UnhandledSource(e.reason)
1224 except OSError as e:
1225 raise UnhandledSource(e.strerror)
1226- return extract(dld_file)
1227+ options = urlparse.parse_qs(url_parts.fragment)
1228+ for key, value in options.items():
1229+ if key in hashlib.algorithms:
1230+ check_hash(dld_file, value, key)
1231+ if checksum:
1232+ check_hash(dld_file, checksum, hash_type)
1233+ return extract(dld_file, dest)
1234
1235=== modified file 'hooks/horizon_hooks.py'
1236--- hooks/horizon_hooks.py 2014-09-12 10:55:03 +0000
1237+++ hooks/horizon_hooks.py 2014-10-01 14:35:37 +0000
1238@@ -32,6 +32,10 @@
1239 enable_ssl,
1240 do_openstack_upgrade
1241 )
1242+from charmhelpers.contrib.network.ip import (
1243+ get_iface_for_address,
1244+ get_netmask_for_address,
1245+)
1246 from charmhelpers.contrib.hahelpers.apache import install_ca_cert
1247 from charmhelpers.contrib.hahelpers.cluster import get_hacluster_config
1248 from charmhelpers.payload.execd import execd_preinstall
1249@@ -114,15 +118,30 @@
1250 def ha_relation_joined():
1251 config = get_hacluster_config()
1252 resources = {
1253- 'res_horizon_vip': 'ocf:heartbeat:IPaddr2',
1254 'res_horizon_haproxy': 'lsb:haproxy'
1255 }
1256- vip_params = 'params ip="{}" cidr_netmask="{}" nic="{}"'.format(
1257- config['vip'], config['vip_cidr'], config['vip_iface'])
1258+
1259 resource_params = {
1260- 'res_horizon_vip': vip_params,
1261 'res_horizon_haproxy': 'op monitor interval="5s"'
1262 }
1263+
1264+ vip_group = []
1265+ for vip in config['vip'].split():
1266+ iface = get_iface_for_address(vip)
1267+ if iface is not None:
1268+ vip_key = 'res_horizon_{}_vip'.format(iface)
1269+ resources[vip_key] = 'ocf:heartbeat:IPaddr2'
1270+ resource_params[vip_key] = (
1271+ 'params ip="{vip}" cidr_netmask="{netmask}"'
1272+ ' nic="{iface}"'.format(vip=vip,
1273+ iface=iface,
1274+ netmask=get_netmask_for_address(vip))
1275+ )
1276+ vip_group.append(vip_key)
1277+
1278+ if len(vip_group) > 1:
1279+ relation_set(groups={'grp_horizon_vips': ' '.join(vip_group)})
1280+
1281 init_services = {
1282 'res_horizon_haproxy': 'haproxy'
1283 }
1284
1285=== modified file 'unit_tests/test_horizon_hooks.py'
1286--- unit_tests/test_horizon_hooks.py 2014-09-12 10:55:03 +0000
1287+++ unit_tests/test_horizon_hooks.py 2014-10-01 14:35:37 +0000
1288@@ -29,7 +29,10 @@
1289 'log',
1290 'execd_preinstall',
1291 'b64decode',
1292- 'os_release']
1293+ 'os_release',
1294+ 'get_iface_for_address',
1295+ 'get_netmask_for_address',
1296+]
1297
1298
1299 def passthrough(value):
1300@@ -42,6 +45,7 @@
1301 super(TestHorizonHooks, self).setUp(hooks, TO_PATCH)
1302 self.config.side_effect = self.test_config.get
1303 self.b64decode.side_effect = passthrough
1304+ hooks.hooks._config_save = False
1305
1306 def _call_hook(self, hookname):
1307 hooks.hooks.execute([
1308@@ -59,14 +63,18 @@
1309 self.os_release.return_value = 'icehouse'
1310 self._call_hook('install')
1311 for pkg in ['nodejs', 'node-less']:
1312- self.assertFalse(pkg in self.filter_installed_packages.call_args[0][0])
1313+ self.assertFalse(
1314+ pkg in self.filter_installed_packages.call_args[0][0]
1315+ )
1316 self.apt_install.assert_called()
1317
1318 def test_install_hook_pre_icehouse_pkgs(self):
1319 self.os_release.return_value = 'grizzly'
1320 self._call_hook('install')
1321 for pkg in ['nodejs', 'node-less']:
1322- self.assertTrue(pkg in self.filter_installed_packages.call_args[0][0])
1323+ self.assertTrue(
1324+ pkg in self.filter_installed_packages.call_args[0][0]
1325+ )
1326 self.apt_install.assert_called()
1327
1328 @patch('charmhelpers.core.host.file_hash')
1329@@ -94,6 +102,8 @@
1330 'vip_iface': 'eth101',
1331 'vip_cidr': '19'
1332 }
1333+ self.get_iface_for_address.return_value = 'eth101'
1334+ self.get_netmask_for_address.return_value = '19'
1335 self.get_hacluster_config.return_value = conf
1336 self._call_hook('ha-relation-joined')
1337 ex_args = {
1338@@ -101,7 +111,7 @@
1339 'init_services': {
1340 'res_horizon_haproxy': 'haproxy'},
1341 'resource_params': {
1342- 'res_horizon_vip':
1343+ 'res_horizon_eth101_vip':
1344 'params ip="192.168.25.163" cidr_netmask="19"'
1345 ' nic="eth101"',
1346 'res_horizon_haproxy': 'op monitor interval="5s"'},
1347@@ -109,7 +119,7 @@
1348 'clones': {
1349 'cl_horizon_haproxy': 'res_horizon_haproxy'},
1350 'resources': {
1351- 'res_horizon_vip': 'ocf:heartbeat:IPaddr2',
1352+ 'res_horizon_eth101_vip': 'ocf:heartbeat:IPaddr2',
1353 'res_horizon_haproxy': 'lsb:haproxy'}
1354 }
1355 self.relation_set.assert_called_with(**ex_args)

Subscribers

People subscribed via source and target branches