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