Merge lp:~corey.bryant/charms/trusty/swift-storage/amulet-updates into lp:~openstack-charmers-archive/charms/trusty/swift-storage/next
- Trusty Tahr (14.04)
- amulet-updates
- Merge into next
Proposed by
Corey Bryant
Status: | Merged |
---|---|
Merged at revision: | 45 |
Proposed branch: | lp:~corey.bryant/charms/trusty/swift-storage/amulet-updates |
Merge into: | lp:~openstack-charmers-archive/charms/trusty/swift-storage/next |
Diff against target: |
1218 lines (+585/-137) 21 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/core/hookenv.py (+17/-4) hooks/charmhelpers/core/host.py (+30/-5) hooks/charmhelpers/core/services/helpers.py (+119/-5) hooks/charmhelpers/fetch/__init__.py (+19/-5) hooks/charmhelpers/fetch/archiveurl.py (+49/-4) tests/00-setup (+5/-5) tests/README (+6/-0) tests/basic_deployment.py (+20/-10) 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/swift-storage/amulet-updates |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
OpenStack Charmers | Pending | ||
Review via email: mp+236518@code.launchpad.net |
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-08-13 13:13:37 +0000 |
3 | +++ Makefile 2014-09-30 13:42:56 +0000 |
4 | @@ -15,7 +15,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 | bin/charm_helpers_sync.py: |
13 | @mkdir -p bin |
14 | |
15 | === modified file 'hooks/charmhelpers/contrib/hahelpers/apache.py' |
16 | --- hooks/charmhelpers/contrib/hahelpers/apache.py 2014-03-27 10:20:58 +0000 |
17 | +++ hooks/charmhelpers/contrib/hahelpers/apache.py 2014-09-30 13:42:56 +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:13:37 +0000 |
52 | +++ hooks/charmhelpers/contrib/hahelpers/cluster.py 2014-09-30 13:42:56 +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:13:37 +0000 |
68 | +++ hooks/charmhelpers/contrib/network/ip.py 2014-09-30 13:42:56 +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-08-13 13:13:37 +0000 |
203 | +++ hooks/charmhelpers/contrib/openstack/amulet/deployment.py 2014-09-30 13:42:56 +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-08-13 13:13:37 +0000 |
278 | +++ hooks/charmhelpers/contrib/openstack/amulet/utils.py 2014-09-30 13:42:56 +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:13:37 +0000 |
303 | +++ hooks/charmhelpers/contrib/openstack/context.py 2014-09-30 13:42:56 +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:13:37 +0000 |
463 | +++ hooks/charmhelpers/contrib/openstack/templates/haproxy.cfg 2014-09-30 13:42:56 +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-16 20:34:18 +0000 |
485 | +++ hooks/charmhelpers/contrib/openstack/templates/openstack_https_frontend 2014-09-30 13:42:56 +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:33:06 +0000 |
521 | +++ hooks/charmhelpers/contrib/openstack/templates/openstack_https_frontend.conf 2014-09-30 13:42:56 +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/core/hookenv.py' |
556 | --- hooks/charmhelpers/core/hookenv.py 2014-09-02 13:21:15 +0000 |
557 | +++ hooks/charmhelpers/core/hookenv.py 2014-09-30 13:42:56 +0000 |
558 | @@ -203,6 +203,17 @@ |
559 | if os.path.exists(self.path): |
560 | self.load_previous() |
561 | |
562 | + def __getitem__(self, key): |
563 | + """For regular dict lookups, check the current juju config first, |
564 | + then the previous (saved) copy. This ensures that user-saved values |
565 | + will be returned by a dict lookup. |
566 | + |
567 | + """ |
568 | + try: |
569 | + return dict.__getitem__(self, key) |
570 | + except KeyError: |
571 | + return (self._prev_dict or {})[key] |
572 | + |
573 | def load_previous(self, path=None): |
574 | """Load previous copy of config from disk. |
575 | |
576 | @@ -475,9 +486,10 @@ |
577 | hooks.execute(sys.argv) |
578 | """ |
579 | |
580 | - def __init__(self): |
581 | + def __init__(self, config_save=True): |
582 | super(Hooks, self).__init__() |
583 | self._hooks = {} |
584 | + self._config_save = config_save |
585 | |
586 | def register(self, name, function): |
587 | """Register a hook""" |
588 | @@ -488,9 +500,10 @@ |
589 | hook_name = os.path.basename(args[0]) |
590 | if hook_name in self._hooks: |
591 | self._hooks[hook_name]() |
592 | - cfg = config() |
593 | - if cfg.implicit_save: |
594 | - cfg.save() |
595 | + if self._config_save: |
596 | + cfg = config() |
597 | + if cfg.implicit_save: |
598 | + cfg.save() |
599 | else: |
600 | raise UnregisteredHookError(hook_name) |
601 | |
602 | |
603 | === modified file 'hooks/charmhelpers/core/host.py' |
604 | --- hooks/charmhelpers/core/host.py 2014-08-26 13:19:40 +0000 |
605 | +++ hooks/charmhelpers/core/host.py 2014-09-30 13:42:56 +0000 |
606 | @@ -68,8 +68,8 @@ |
607 | """Determine whether a system service is available""" |
608 | try: |
609 | subprocess.check_output(['service', service_name, 'status'], stderr=subprocess.STDOUT) |
610 | - except subprocess.CalledProcessError: |
611 | - return False |
612 | + except subprocess.CalledProcessError as e: |
613 | + return 'unrecognized service' not in e.output |
614 | else: |
615 | return True |
616 | |
617 | @@ -209,10 +209,15 @@ |
618 | return system_mounts |
619 | |
620 | |
621 | -def file_hash(path): |
622 | - """Generate a md5 hash of the contents of 'path' or None if not found """ |
623 | +def file_hash(path, hash_type='md5'): |
624 | + """ |
625 | + Generate a hash checksum of the contents of 'path' or None if not found. |
626 | + |
627 | + :param str hash_type: Any hash alrgorithm supported by :mod:`hashlib`, |
628 | + such as md5, sha1, sha256, sha512, etc. |
629 | + """ |
630 | if os.path.exists(path): |
631 | - h = hashlib.md5() |
632 | + h = getattr(hashlib, hash_type)() |
633 | with open(path, 'r') as source: |
634 | h.update(source.read()) # IGNORE:E1101 - it does have update |
635 | return h.hexdigest() |
636 | @@ -220,6 +225,26 @@ |
637 | return None |
638 | |
639 | |
640 | +def check_hash(path, checksum, hash_type='md5'): |
641 | + """ |
642 | + Validate a file using a cryptographic checksum. |
643 | + |
644 | + :param str checksum: Value of the checksum used to validate the file. |
645 | + :param str hash_type: Hash algorithm used to generate `checksum`. |
646 | + Can be any hash alrgorithm supported by :mod:`hashlib`, |
647 | + such as md5, sha1, sha256, sha512, etc. |
648 | + :raises ChecksumError: If the file fails the checksum |
649 | + |
650 | + """ |
651 | + actual_checksum = file_hash(path, hash_type) |
652 | + if checksum != actual_checksum: |
653 | + raise ChecksumError("'%s' != '%s'" % (checksum, actual_checksum)) |
654 | + |
655 | + |
656 | +class ChecksumError(ValueError): |
657 | + pass |
658 | + |
659 | + |
660 | def restart_on_change(restart_map, stopstart=False): |
661 | """Restart services based on configuration files changing |
662 | |
663 | |
664 | === modified file 'hooks/charmhelpers/core/services/helpers.py' |
665 | --- hooks/charmhelpers/core/services/helpers.py 2014-08-13 13:13:37 +0000 |
666 | +++ hooks/charmhelpers/core/services/helpers.py 2014-09-30 13:42:56 +0000 |
667 | @@ -1,3 +1,5 @@ |
668 | +import os |
669 | +import yaml |
670 | from charmhelpers.core import hookenv |
671 | from charmhelpers.core import templating |
672 | |
673 | @@ -19,15 +21,21 @@ |
674 | the `name` attribute that are complete will used to populate the dictionary |
675 | values (see `get_data`, below). |
676 | |
677 | - The generated context will be namespaced under the interface type, to prevent |
678 | - potential naming conflicts. |
679 | + The generated context will be namespaced under the relation :attr:`name`, |
680 | + to prevent potential naming conflicts. |
681 | + |
682 | + :param str name: Override the relation :attr:`name`, since it can vary from charm to charm |
683 | + :param list additional_required_keys: Extend the list of :attr:`required_keys` |
684 | """ |
685 | name = None |
686 | interface = None |
687 | required_keys = [] |
688 | |
689 | - def __init__(self, *args, **kwargs): |
690 | - super(RelationContext, self).__init__(*args, **kwargs) |
691 | + def __init__(self, name=None, additional_required_keys=None): |
692 | + if name is not None: |
693 | + self.name = name |
694 | + if additional_required_keys is not None: |
695 | + self.required_keys.extend(additional_required_keys) |
696 | self.get_data() |
697 | |
698 | def __bool__(self): |
699 | @@ -101,9 +109,115 @@ |
700 | return {} |
701 | |
702 | |
703 | +class MysqlRelation(RelationContext): |
704 | + """ |
705 | + Relation context for the `mysql` interface. |
706 | + |
707 | + :param str name: Override the relation :attr:`name`, since it can vary from charm to charm |
708 | + :param list additional_required_keys: Extend the list of :attr:`required_keys` |
709 | + """ |
710 | + name = 'db' |
711 | + interface = 'mysql' |
712 | + required_keys = ['host', 'user', 'password', 'database'] |
713 | + |
714 | + |
715 | +class HttpRelation(RelationContext): |
716 | + """ |
717 | + Relation context for the `http` interface. |
718 | + |
719 | + :param str name: Override the relation :attr:`name`, since it can vary from charm to charm |
720 | + :param list additional_required_keys: Extend the list of :attr:`required_keys` |
721 | + """ |
722 | + name = 'website' |
723 | + interface = 'http' |
724 | + required_keys = ['host', 'port'] |
725 | + |
726 | + def provide_data(self): |
727 | + return { |
728 | + 'host': hookenv.unit_get('private-address'), |
729 | + 'port': 80, |
730 | + } |
731 | + |
732 | + |
733 | +class RequiredConfig(dict): |
734 | + """ |
735 | + Data context that loads config options with one or more mandatory options. |
736 | + |
737 | + Once the required options have been changed from their default values, all |
738 | + config options will be available, namespaced under `config` to prevent |
739 | + potential naming conflicts (for example, between a config option and a |
740 | + relation property). |
741 | + |
742 | + :param list *args: List of options that must be changed from their default values. |
743 | + """ |
744 | + |
745 | + def __init__(self, *args): |
746 | + self.required_options = args |
747 | + self['config'] = hookenv.config() |
748 | + with open(os.path.join(hookenv.charm_dir(), 'config.yaml')) as fp: |
749 | + self.config = yaml.load(fp).get('options', {}) |
750 | + |
751 | + def __bool__(self): |
752 | + for option in self.required_options: |
753 | + if option not in self['config']: |
754 | + return False |
755 | + current_value = self['config'][option] |
756 | + default_value = self.config[option].get('default') |
757 | + if current_value == default_value: |
758 | + return False |
759 | + if current_value in (None, '') and default_value in (None, ''): |
760 | + return False |
761 | + return True |
762 | + |
763 | + def __nonzero__(self): |
764 | + return self.__bool__() |
765 | + |
766 | + |
767 | +class StoredContext(dict): |
768 | + """ |
769 | + A data context that always returns the data that it was first created with. |
770 | + |
771 | + This is useful to do a one-time generation of things like passwords, that |
772 | + will thereafter use the same value that was originally generated, instead |
773 | + of generating a new value each time it is run. |
774 | + """ |
775 | + def __init__(self, file_name, config_data): |
776 | + """ |
777 | + If the file exists, populate `self` with the data from the file. |
778 | + Otherwise, populate with the given data and persist it to the file. |
779 | + """ |
780 | + if os.path.exists(file_name): |
781 | + self.update(self.read_context(file_name)) |
782 | + else: |
783 | + self.store_context(file_name, config_data) |
784 | + self.update(config_data) |
785 | + |
786 | + def store_context(self, file_name, config_data): |
787 | + if not os.path.isabs(file_name): |
788 | + file_name = os.path.join(hookenv.charm_dir(), file_name) |
789 | + with open(file_name, 'w') as file_stream: |
790 | + os.fchmod(file_stream.fileno(), 0600) |
791 | + yaml.dump(config_data, file_stream) |
792 | + |
793 | + def read_context(self, file_name): |
794 | + if not os.path.isabs(file_name): |
795 | + file_name = os.path.join(hookenv.charm_dir(), file_name) |
796 | + with open(file_name, 'r') as file_stream: |
797 | + data = yaml.load(file_stream) |
798 | + if not data: |
799 | + raise OSError("%s is empty" % file_name) |
800 | + return data |
801 | + |
802 | + |
803 | class TemplateCallback(ManagerCallback): |
804 | """ |
805 | - Callback class that will render a template, for use as a ready action. |
806 | + Callback class that will render a Jinja2 template, for use as a ready action. |
807 | + |
808 | + :param str source: The template source file, relative to `$CHARM_DIR/templates` |
809 | + :param str target: The target to write the rendered template to |
810 | + :param str owner: The owner of the rendered file |
811 | + :param str group: The group of the rendered file |
812 | + :param int perms: The permissions of the rendered file |
813 | """ |
814 | def __init__(self, source, target, owner='root', group='root', perms=0444): |
815 | self.source = source |
816 | |
817 | === modified file 'hooks/charmhelpers/fetch/__init__.py' |
818 | --- hooks/charmhelpers/fetch/__init__.py 2014-08-26 13:19:40 +0000 |
819 | +++ hooks/charmhelpers/fetch/__init__.py 2014-09-30 13:42:56 +0000 |
820 | @@ -208,7 +208,8 @@ |
821 | """Add a package source to this system. |
822 | |
823 | @param source: a URL or sources.list entry, as supported by |
824 | - add-apt-repository(1). Examples: |
825 | + add-apt-repository(1). Examples:: |
826 | + |
827 | ppa:charmers/example |
828 | deb https://stub:key@private.example.com/ubuntu trusty main |
829 | |
830 | @@ -311,22 +312,35 @@ |
831 | apt_update(fatal=True) |
832 | |
833 | |
834 | -def install_remote(source): |
835 | +def install_remote(source, *args, **kwargs): |
836 | """ |
837 | Install a file tree from a remote source |
838 | |
839 | The specified source should be a url of the form: |
840 | scheme://[host]/path[#[option=value][&...]] |
841 | |
842 | - Schemes supported are based on this modules submodules |
843 | - Options supported are submodule-specific""" |
844 | + Schemes supported are based on this modules submodules. |
845 | + Options supported are submodule-specific. |
846 | + Additional arguments are passed through to the submodule. |
847 | + |
848 | + For example:: |
849 | + |
850 | + dest = install_remote('http://example.com/archive.tgz', |
851 | + checksum='deadbeef', |
852 | + hash_type='sha1') |
853 | + |
854 | + This will download `archive.tgz`, validate it using SHA1 and, if |
855 | + the file is ok, extract it and return the directory in which it |
856 | + was extracted. If the checksum fails, it will raise |
857 | + :class:`charmhelpers.core.host.ChecksumError`. |
858 | + """ |
859 | # We ONLY check for True here because can_handle may return a string |
860 | # explaining why it can't handle a given source. |
861 | handlers = [h for h in plugins() if h.can_handle(source) is True] |
862 | installed_to = None |
863 | for handler in handlers: |
864 | try: |
865 | - installed_to = handler.install(source) |
866 | + installed_to = handler.install(source, *args, **kwargs) |
867 | except UnhandledSource: |
868 | pass |
869 | if not installed_to: |
870 | |
871 | === modified file 'hooks/charmhelpers/fetch/archiveurl.py' |
872 | --- hooks/charmhelpers/fetch/archiveurl.py 2014-03-20 13:47:54 +0000 |
873 | +++ hooks/charmhelpers/fetch/archiveurl.py 2014-09-30 13:42:56 +0000 |
874 | @@ -1,6 +1,8 @@ |
875 | import os |
876 | import urllib2 |
877 | +from urllib import urlretrieve |
878 | import urlparse |
879 | +import hashlib |
880 | |
881 | from charmhelpers.fetch import ( |
882 | BaseFetchHandler, |
883 | @@ -10,11 +12,19 @@ |
884 | get_archive_handler, |
885 | extract, |
886 | ) |
887 | -from charmhelpers.core.host import mkdir |
888 | +from charmhelpers.core.host import mkdir, check_hash |
889 | |
890 | |
891 | class ArchiveUrlFetchHandler(BaseFetchHandler): |
892 | - """Handler for archives via generic URLs""" |
893 | + """ |
894 | + Handler to download archive files from arbitrary URLs. |
895 | + |
896 | + Can fetch from http, https, ftp, and file URLs. |
897 | + |
898 | + Can install either tarballs (.tar, .tgz, .tbz2, etc) or zip files. |
899 | + |
900 | + Installs the contents of the archive in $CHARM_DIR/fetched/. |
901 | + """ |
902 | def can_handle(self, source): |
903 | url_parts = self.parse_url(source) |
904 | if url_parts.scheme not in ('http', 'https', 'ftp', 'file'): |
905 | @@ -24,6 +34,12 @@ |
906 | return False |
907 | |
908 | def download(self, source, dest): |
909 | + """ |
910 | + Download an archive file. |
911 | + |
912 | + :param str source: URL pointing to an archive file. |
913 | + :param str dest: Local path location to download archive file to. |
914 | + """ |
915 | # propogate all exceptions |
916 | # URLError, OSError, etc |
917 | proto, netloc, path, params, query, fragment = urlparse.urlparse(source) |
918 | @@ -48,7 +64,30 @@ |
919 | os.unlink(dest) |
920 | raise e |
921 | |
922 | - def install(self, source): |
923 | + # Mandatory file validation via Sha1 or MD5 hashing. |
924 | + def download_and_validate(self, url, hashsum, validate="sha1"): |
925 | + tempfile, headers = urlretrieve(url) |
926 | + check_hash(tempfile, hashsum, validate) |
927 | + return tempfile |
928 | + |
929 | + def install(self, source, dest=None, checksum=None, hash_type='sha1'): |
930 | + """ |
931 | + Download and install an archive file, with optional checksum validation. |
932 | + |
933 | + The checksum can also be given on the `source` URL's fragment. |
934 | + For example:: |
935 | + |
936 | + handler.install('http://example.com/file.tgz#sha1=deadbeef') |
937 | + |
938 | + :param str source: URL pointing to an archive file. |
939 | + :param str dest: Local destination path to install to. If not given, |
940 | + installs to `$CHARM_DIR/archives/archive_file_name`. |
941 | + :param str checksum: If given, validate the archive file after download. |
942 | + :param str hash_type: Algorithm used to generate `checksum`. |
943 | + Can be any hash alrgorithm supported by :mod:`hashlib`, |
944 | + such as md5, sha1, sha256, sha512, etc. |
945 | + |
946 | + """ |
947 | url_parts = self.parse_url(source) |
948 | dest_dir = os.path.join(os.environ.get('CHARM_DIR'), 'fetched') |
949 | if not os.path.exists(dest_dir): |
950 | @@ -60,4 +99,10 @@ |
951 | raise UnhandledSource(e.reason) |
952 | except OSError as e: |
953 | raise UnhandledSource(e.strerror) |
954 | - return extract(dld_file) |
955 | + options = urlparse.parse_qs(url_parts.fragment) |
956 | + for key, value in options.items(): |
957 | + if key in hashlib.algorithms: |
958 | + check_hash(dld_file, value, key) |
959 | + if checksum: |
960 | + check_hash(dld_file, checksum, hash_type) |
961 | + return extract(dld_file, dest) |
962 | |
963 | === modified file 'tests/00-setup' |
964 | --- tests/00-setup 2014-07-11 16:41:12 +0000 |
965 | +++ tests/00-setup 2014-09-30 13:42:56 +0000 |
966 | @@ -4,8 +4,8 @@ |
967 | |
968 | sudo add-apt-repository --yes ppa:juju/stable |
969 | sudo apt-get update --yes |
970 | -sudo apt-get install --yes python-amulet |
971 | -sudo apt-get install --yes python-swiftclient |
972 | -sudo apt-get install --yes python-glanceclient |
973 | -sudo apt-get install --yes python-keystoneclient |
974 | -sudo apt-get install --yes python-novaclient |
975 | +sudo apt-get install --yes python-amulet \ |
976 | + python-swiftclient \ |
977 | + python-glanceclient \ |
978 | + python-keystoneclient \ |
979 | + python-novaclient |
980 | |
981 | === modified file 'tests/README' |
982 | --- tests/README 2014-07-11 16:41:12 +0000 |
983 | +++ tests/README 2014-09-30 13:42:56 +0000 |
984 | @@ -1,6 +1,12 @@ |
985 | This directory provides Amulet tests that focus on verification of swift-storage |
986 | deployments. |
987 | |
988 | +In order to run tests, you'll need charm-tools installed (in addition to |
989 | +juju, of course): |
990 | + sudo add-apt-repository ppa:juju/stable |
991 | + sudo apt-get update |
992 | + sudo apt-get install charm-tools |
993 | + |
994 | If you use a web proxy server to access the web, you'll need to set the |
995 | AMULET_HTTP_PROXY environment variable to the http URL of the proxy server. |
996 | |
997 | |
998 | === modified file 'tests/basic_deployment.py' |
999 | --- tests/basic_deployment.py 2014-07-11 16:41:12 +0000 |
1000 | +++ tests/basic_deployment.py 2014-09-30 13:42:56 +0000 |
1001 | @@ -20,10 +20,10 @@ |
1002 | class SwiftStorageBasicDeployment(OpenStackAmuletDeployment): |
1003 | """Amulet tests on a basic swift-storage deployment.""" |
1004 | |
1005 | - def __init__(self, series, openstack=None, source=None): |
1006 | + def __init__(self, series, openstack=None, source=None, stable=False): |
1007 | """Deploy the entire test environment.""" |
1008 | super(SwiftStorageBasicDeployment, self).__init__(series, openstack, |
1009 | - source) |
1010 | + source, stable) |
1011 | self._add_services() |
1012 | self._add_relations() |
1013 | self._configure_services() |
1014 | @@ -31,12 +31,15 @@ |
1015 | self._initialize_tests() |
1016 | |
1017 | def _add_services(self): |
1018 | - """Add the service that we're testing, including the number of units, |
1019 | - where swift-storage is local, and the other charms are from |
1020 | - the charm store.""" |
1021 | - this_service = ('swift-storage', 1) |
1022 | - other_services = [('mysql', 1), |
1023 | - ('keystone', 1), ('glance', 1), ('swift-proxy', 1)] |
1024 | + """Add services |
1025 | + |
1026 | + Add the services that we're testing, where swift-storage is local, |
1027 | + and the rest of the service are from lp branches that are |
1028 | + compatible with the local charm (e.g. stable or next). |
1029 | + """ |
1030 | + this_service = {'name': 'swift-storage'} |
1031 | + other_services = [{'name': 'mysql'}, {'name': 'keystone'}, |
1032 | + {'name': 'glance'}, {'name': 'swift-proxy'}] |
1033 | super(SwiftStorageBasicDeployment, self)._add_services(this_service, |
1034 | other_services) |
1035 | |
1036 | @@ -249,9 +252,14 @@ |
1037 | message = u.relation_error('swift-proxy swift-storage', ret) |
1038 | amulet.raise_status(amulet.FAIL, msg=message) |
1039 | |
1040 | - def test_restart_on_config_change(self): |
1041 | + def test_z_restart_on_config_change(self): |
1042 | """Verify that the specified services are restarted when the config |
1043 | - is changed.""" |
1044 | + is changed. |
1045 | + |
1046 | + Note(coreycb): The method name with the _z_ is a little odd |
1047 | + but it forces the test to run last. It just makes things |
1048 | + easier because restarting services requires re-authorization. |
1049 | + """ |
1050 | # NOTE(coreycb): Skipping failing test on until resolved. This test |
1051 | # fails because the config file's last mod time is |
1052 | # slightly after the process' last mod time. |
1053 | @@ -282,6 +290,8 @@ |
1054 | config = '/etc/swift/{}'.format(conf) |
1055 | if not u.service_restarted(self.swift_storage_sentry, s, config, |
1056 | pgrep_full=True, sleep_time=time): |
1057 | + self.d.configure('swift-storage', |
1058 | + {'object-server-threads-per-disk': '4'}) |
1059 | msg = "service {} didn't restart after config change".format(s) |
1060 | amulet.raise_status(amulet.FAIL, msg=msg) |
1061 | time = 0 |
1062 | |
1063 | === modified file 'tests/charmhelpers/contrib/amulet/deployment.py' |
1064 | --- tests/charmhelpers/contrib/amulet/deployment.py 2014-08-13 13:13:37 +0000 |
1065 | +++ tests/charmhelpers/contrib/amulet/deployment.py 2014-09-30 13:42:56 +0000 |
1066 | @@ -24,25 +24,31 @@ |
1067 | """Add services. |
1068 | |
1069 | Add services to the deployment where this_service is the local charm |
1070 | - that we're focused on testing and other_services are the other |
1071 | - charms that come from the charm store. |
1072 | + that we're testing and other_services are the other services that |
1073 | + are being used in the local amulet tests. |
1074 | """ |
1075 | - name, units = range(2) |
1076 | - |
1077 | - if this_service[name] != os.path.basename(os.getcwd()): |
1078 | - s = this_service[name] |
1079 | + if this_service['name'] != os.path.basename(os.getcwd()): |
1080 | + s = this_service['name'] |
1081 | msg = "The charm's root directory name needs to be {}".format(s) |
1082 | amulet.raise_status(amulet.FAIL, msg=msg) |
1083 | |
1084 | - self.d.add(this_service[name], units=this_service[units]) |
1085 | + if 'units' not in this_service: |
1086 | + this_service['units'] = 1 |
1087 | + |
1088 | + self.d.add(this_service['name'], units=this_service['units']) |
1089 | |
1090 | for svc in other_services: |
1091 | - if self.series: |
1092 | - self.d.add(svc[name], |
1093 | - charm='cs:{}/{}'.format(self.series, svc[name]), |
1094 | - units=svc[units]) |
1095 | + if 'location' in svc: |
1096 | + branch_location = svc['location'] |
1097 | + elif self.series: |
1098 | + branch_location = 'cs:{}/{}'.format(self.series, svc['name']), |
1099 | else: |
1100 | - self.d.add(svc[name], units=svc[units]) |
1101 | + branch_location = None |
1102 | + |
1103 | + if 'units' not in svc: |
1104 | + svc['units'] = 1 |
1105 | + |
1106 | + self.d.add(svc['name'], charm=branch_location, units=svc['units']) |
1107 | |
1108 | def _add_relations(self, relations): |
1109 | """Add all of the relations for the services.""" |
1110 | @@ -57,7 +63,7 @@ |
1111 | def _deploy(self): |
1112 | """Deploy environment and wait for all hooks to finish executing.""" |
1113 | try: |
1114 | - self.d.setup() |
1115 | + self.d.setup(timeout=900) |
1116 | self.d.sentry.wait(timeout=900) |
1117 | except amulet.helpers.TimeoutError: |
1118 | amulet.raise_status(amulet.FAIL, msg="Deployment timed out") |
1119 | |
1120 | === modified file 'tests/charmhelpers/contrib/openstack/amulet/deployment.py' |
1121 | --- tests/charmhelpers/contrib/openstack/amulet/deployment.py 2014-08-13 13:13:37 +0000 |
1122 | +++ tests/charmhelpers/contrib/openstack/amulet/deployment.py 2014-09-30 13:42:56 +0000 |
1123 | @@ -10,32 +10,62 @@ |
1124 | that is specifically for use by OpenStack charms. |
1125 | """ |
1126 | |
1127 | - def __init__(self, series=None, openstack=None, source=None): |
1128 | + def __init__(self, series=None, openstack=None, source=None, stable=True): |
1129 | """Initialize the deployment environment.""" |
1130 | super(OpenStackAmuletDeployment, self).__init__(series) |
1131 | self.openstack = openstack |
1132 | self.source = source |
1133 | + self.stable = stable |
1134 | + # Note(coreycb): this needs to be changed when new next branches come |
1135 | + # out. |
1136 | + self.current_next = "trusty" |
1137 | + |
1138 | + def _determine_branch_locations(self, other_services): |
1139 | + """Determine the branch locations for the other services. |
1140 | + |
1141 | + Determine if the local branch being tested is derived from its |
1142 | + stable or next (dev) branch, and based on this, use the corresonding |
1143 | + stable or next branches for the other_services.""" |
1144 | + base_charms = ['mysql', 'mongodb', 'rabbitmq-server'] |
1145 | + |
1146 | + if self.stable: |
1147 | + for svc in other_services: |
1148 | + temp = 'lp:charms/{}' |
1149 | + svc['location'] = temp.format(svc['name']) |
1150 | + else: |
1151 | + for svc in other_services: |
1152 | + if svc['name'] in base_charms: |
1153 | + temp = 'lp:charms/{}' |
1154 | + svc['location'] = temp.format(svc['name']) |
1155 | + else: |
1156 | + temp = 'lp:~openstack-charmers/charms/{}/{}/next' |
1157 | + svc['location'] = temp.format(self.current_next, |
1158 | + svc['name']) |
1159 | + return other_services |
1160 | |
1161 | def _add_services(self, this_service, other_services): |
1162 | - """Add services to the deployment and set openstack-origin.""" |
1163 | + """Add services to the deployment and set openstack-origin/source.""" |
1164 | + other_services = self._determine_branch_locations(other_services) |
1165 | + |
1166 | super(OpenStackAmuletDeployment, self)._add_services(this_service, |
1167 | other_services) |
1168 | - name = 0 |
1169 | + |
1170 | services = other_services |
1171 | services.append(this_service) |
1172 | - use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph'] |
1173 | + use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph', |
1174 | + 'ceph-osd', 'ceph-radosgw'] |
1175 | |
1176 | if self.openstack: |
1177 | for svc in services: |
1178 | - if svc[name] not in use_source: |
1179 | + if svc['name'] not in use_source: |
1180 | config = {'openstack-origin': self.openstack} |
1181 | - self.d.configure(svc[name], config) |
1182 | + self.d.configure(svc['name'], config) |
1183 | |
1184 | if self.source: |
1185 | for svc in services: |
1186 | - if svc[name] in use_source: |
1187 | + if svc['name'] in use_source: |
1188 | config = {'source': self.source} |
1189 | - self.d.configure(svc[name], config) |
1190 | + self.d.configure(svc['name'], config) |
1191 | |
1192 | def _configure_services(self, configs): |
1193 | """Configure all of the services.""" |
1194 | |
1195 | === modified file 'tests/charmhelpers/contrib/openstack/amulet/utils.py' |
1196 | --- tests/charmhelpers/contrib/openstack/amulet/utils.py 2014-08-13 13:13:37 +0000 |
1197 | +++ tests/charmhelpers/contrib/openstack/amulet/utils.py 2014-09-30 13:42:56 +0000 |
1198 | @@ -187,15 +187,16 @@ |
1199 | |
1200 | f = opener.open("http://download.cirros-cloud.net/version/released") |
1201 | version = f.read().strip() |
1202 | - cirros_img = "tests/cirros-{}-x86_64-disk.img".format(version) |
1203 | + cirros_img = "cirros-{}-x86_64-disk.img".format(version) |
1204 | + local_path = os.path.join('tests', cirros_img) |
1205 | |
1206 | - if not os.path.exists(cirros_img): |
1207 | + if not os.path.exists(local_path): |
1208 | cirros_url = "http://{}/{}/{}".format("download.cirros-cloud.net", |
1209 | version, cirros_img) |
1210 | - opener.retrieve(cirros_url, cirros_img) |
1211 | + opener.retrieve(cirros_url, local_path) |
1212 | f.close() |
1213 | |
1214 | - with open(cirros_img) as f: |
1215 | + with open(local_path) as f: |
1216 | image = glance.images.create(name=image_name, is_public=True, |
1217 | disk_format='qcow2', |
1218 | container_format='bare', data=f) |