Merge lp:~corey.bryant/charms/trusty/nova-compute/amulet-updates into lp:~openstack-charmers-archive/charms/trusty/nova-compute/next
- Trusty Tahr (14.04)
- amulet-updates
- Merge into 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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
OpenStack Charmers | Pending | ||
Review via email:
|
Commit message
Description of the change
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) |