Merge lp:~corey.bryant/charms/trusty/nova-compute/amulet-updates into lp:~openstack-charmers-archive/charms/trusty/nova-compute/next

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

Subscribers

People subscribed via source and target branches