Merge lp:~1chb1n/charms/trusty/nova-cell/next-amulet-debug-and-makefile into lp:~openstack-charmers/charms/trusty/nova-cell/next
- Trusty Tahr (14.04)
- next-amulet-debug-and-makefile
- Merge into next
Status: | Merged |
---|---|
Merged at revision: | 65 |
Proposed branch: | lp:~1chb1n/charms/trusty/nova-cell/next-amulet-debug-and-makefile |
Merge into: | lp:~openstack-charmers/charms/trusty/nova-cell/next |
Diff against target: |
2271 lines (+1460/-208) 20 files modified
hooks/charmhelpers/contrib/network/ip.py (+84/-1) hooks/charmhelpers/contrib/openstack/amulet/deployment.py (+34/-5) hooks/charmhelpers/contrib/openstack/context.py (+289/-15) hooks/charmhelpers/contrib/openstack/files/__init__.py (+18/-0) hooks/charmhelpers/contrib/openstack/ip.py (+37/-0) hooks/charmhelpers/contrib/openstack/neutron.py (+83/-0) hooks/charmhelpers/contrib/openstack/utils.py (+142/-141) hooks/charmhelpers/contrib/python/packages.py (+2/-2) hooks/charmhelpers/core/fstab.py (+4/-4) hooks/charmhelpers/core/hookenv.py (+40/-1) hooks/charmhelpers/core/host.py (+10/-6) hooks/charmhelpers/core/services/helpers.py (+12/-4) hooks/charmhelpers/core/strutils.py (+42/-0) hooks/charmhelpers/core/sysctl.py (+13/-7) hooks/charmhelpers/core/templating.py (+3/-3) hooks/charmhelpers/core/unitdata.py (+477/-0) hooks/charmhelpers/fetch/archiveurl.py (+10/-10) hooks/charmhelpers/fetch/giturl.py (+1/-1) tests/charmhelpers/contrib/amulet/utils.py (+125/-3) tests/charmhelpers/contrib/openstack/amulet/deployment.py (+34/-5) |
To merge this branch: | bzr merge lp:~1chb1n/charms/trusty/nova-cell/next-amulet-debug-and-makefile |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
OpenStack Charmers | Pending | ||
Review via email: mp+256591@code.launchpad.net |
Commit message
Description of the change
auto sync charmhelpers
uosci-testing-bot (uosci-testing-bot) wrote : | # |
uosci-testing-bot (uosci-testing-bot) wrote : | # |
charm_unit_test #3314 nova-cell-next for 1chb1n mp256591
UNIT OK: passed
uosci-testing-bot (uosci-testing-bot) wrote : | # |
charm_amulet_test #3281 nova-cell-next for 1chb1n mp256591
AMULET FAIL: amulet-test failed
AMULET Results (max last 2 lines):
make: *** [test] Error 1
ERROR:root:Make target returned non-zero.
Full amulet test output: http://
Build: http://
uosci-testing-bot (uosci-testing-bot) wrote : | # |
charm_lint_check #3550 nova-cell-next for 1chb1n mp256591
LINT OK: passed
uosci-testing-bot (uosci-testing-bot) wrote : | # |
charm_lint_check #3560 nova-cell-next for 1chb1n mp256591
LINT OK: passed
uosci-testing-bot (uosci-testing-bot) wrote : | # |
charm_unit_test #3348 nova-cell-next for 1chb1n mp256591
UNIT OK: passed
uosci-testing-bot (uosci-testing-bot) wrote : | # |
charm_amulet_test #3317 nova-cell-next for 1chb1n mp256591
AMULET FAIL: amulet-test failed
AMULET Results (max last 2 lines):
make: *** [test] Error 1
ERROR:root:Make target returned non-zero.
Full amulet test output: http://
Build: http://
Ryan Beisner (1chb1n) wrote : | # |
The amulet fail is actually due to missing / no tests in this charm.
00:01:08.349 juju-test CRITICAL: No tests were found
Preview Diff
1 | === modified file 'hooks/charmhelpers/contrib/network/ip.py' |
2 | --- hooks/charmhelpers/contrib/network/ip.py 2015-01-29 13:02:55 +0000 |
3 | +++ hooks/charmhelpers/contrib/network/ip.py 2015-04-16 21:56:47 +0000 |
4 | @@ -17,13 +17,16 @@ |
5 | import glob |
6 | import re |
7 | import subprocess |
8 | +import six |
9 | +import socket |
10 | |
11 | from functools import partial |
12 | |
13 | from charmhelpers.core.hookenv import unit_get |
14 | from charmhelpers.fetch import apt_install |
15 | from charmhelpers.core.hookenv import ( |
16 | - log |
17 | + log, |
18 | + WARNING, |
19 | ) |
20 | |
21 | try: |
22 | @@ -365,3 +368,83 @@ |
23 | return True |
24 | |
25 | return False |
26 | + |
27 | + |
28 | +def is_ip(address): |
29 | + """ |
30 | + Returns True if address is a valid IP address. |
31 | + """ |
32 | + try: |
33 | + # Test to see if already an IPv4 address |
34 | + socket.inet_aton(address) |
35 | + return True |
36 | + except socket.error: |
37 | + return False |
38 | + |
39 | + |
40 | +def ns_query(address): |
41 | + try: |
42 | + import dns.resolver |
43 | + except ImportError: |
44 | + apt_install('python-dnspython') |
45 | + import dns.resolver |
46 | + |
47 | + if isinstance(address, dns.name.Name): |
48 | + rtype = 'PTR' |
49 | + elif isinstance(address, six.string_types): |
50 | + rtype = 'A' |
51 | + else: |
52 | + return None |
53 | + |
54 | + answers = dns.resolver.query(address, rtype) |
55 | + if answers: |
56 | + return str(answers[0]) |
57 | + return None |
58 | + |
59 | + |
60 | +def get_host_ip(hostname, fallback=None): |
61 | + """ |
62 | + Resolves the IP for a given hostname, or returns |
63 | + the input if it is already an IP. |
64 | + """ |
65 | + if is_ip(hostname): |
66 | + return hostname |
67 | + |
68 | + ip_addr = ns_query(hostname) |
69 | + if not ip_addr: |
70 | + try: |
71 | + ip_addr = socket.gethostbyname(hostname) |
72 | + except: |
73 | + log("Failed to resolve hostname '%s'" % (hostname), |
74 | + level=WARNING) |
75 | + return fallback |
76 | + return ip_addr |
77 | + |
78 | + |
79 | +def get_hostname(address, fqdn=True): |
80 | + """ |
81 | + Resolves hostname for given IP, or returns the input |
82 | + if it is already a hostname. |
83 | + """ |
84 | + if is_ip(address): |
85 | + try: |
86 | + import dns.reversename |
87 | + except ImportError: |
88 | + apt_install("python-dnspython") |
89 | + import dns.reversename |
90 | + |
91 | + rev = dns.reversename.from_address(address) |
92 | + result = ns_query(rev) |
93 | + if not result: |
94 | + return None |
95 | + else: |
96 | + result = address |
97 | + |
98 | + if fqdn: |
99 | + # strip trailing . |
100 | + if result.endswith('.'): |
101 | + return result[:-1] |
102 | + else: |
103 | + return result |
104 | + else: |
105 | + return result.split('.')[0] |
106 | |
107 | === modified file 'hooks/charmhelpers/contrib/openstack/amulet/deployment.py' |
108 | --- hooks/charmhelpers/contrib/openstack/amulet/deployment.py 2015-01-29 13:02:55 +0000 |
109 | +++ hooks/charmhelpers/contrib/openstack/amulet/deployment.py 2015-04-16 21:56:47 +0000 |
110 | @@ -15,6 +15,7 @@ |
111 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
112 | |
113 | import six |
114 | +from collections import OrderedDict |
115 | from charmhelpers.contrib.amulet.deployment import ( |
116 | AmuletDeployment |
117 | ) |
118 | @@ -43,7 +44,7 @@ |
119 | Determine if the local branch being tested is derived from its |
120 | stable or next (dev) branch, and based on this, use the corresonding |
121 | stable or next branches for the other_services.""" |
122 | - base_charms = ['mysql', 'mongodb', 'rabbitmq-server'] |
123 | + base_charms = ['mysql', 'mongodb'] |
124 | |
125 | if self.stable: |
126 | for svc in other_services: |
127 | @@ -71,16 +72,19 @@ |
128 | services.append(this_service) |
129 | use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph', |
130 | 'ceph-osd', 'ceph-radosgw'] |
131 | + # Openstack subordinate charms do not expose an origin option as that |
132 | + # is controlled by the principle |
133 | + ignore = ['neutron-openvswitch'] |
134 | |
135 | if self.openstack: |
136 | for svc in services: |
137 | - if svc['name'] not in use_source: |
138 | + if svc['name'] not in use_source + ignore: |
139 | config = {'openstack-origin': self.openstack} |
140 | self.d.configure(svc['name'], config) |
141 | |
142 | if self.source: |
143 | for svc in services: |
144 | - if svc['name'] in use_source: |
145 | + if svc['name'] in use_source and svc['name'] not in ignore: |
146 | config = {'source': self.source} |
147 | self.d.configure(svc['name'], config) |
148 | |
149 | @@ -97,12 +101,37 @@ |
150 | """ |
151 | (self.precise_essex, self.precise_folsom, self.precise_grizzly, |
152 | self.precise_havana, self.precise_icehouse, |
153 | - self.trusty_icehouse) = range(6) |
154 | + self.trusty_icehouse, self.trusty_juno, self.trusty_kilo, |
155 | + self.utopic_juno, self.vivid_kilo) = range(10) |
156 | releases = { |
157 | ('precise', None): self.precise_essex, |
158 | ('precise', 'cloud:precise-folsom'): self.precise_folsom, |
159 | ('precise', 'cloud:precise-grizzly'): self.precise_grizzly, |
160 | ('precise', 'cloud:precise-havana'): self.precise_havana, |
161 | ('precise', 'cloud:precise-icehouse'): self.precise_icehouse, |
162 | - ('trusty', None): self.trusty_icehouse} |
163 | + ('trusty', None): self.trusty_icehouse, |
164 | + ('trusty', 'cloud:trusty-juno'): self.trusty_juno, |
165 | + ('trusty', 'cloud:trusty-kilo'): self.trusty_kilo, |
166 | + ('utopic', None): self.utopic_juno, |
167 | + ('vivid', None): self.vivid_kilo} |
168 | return releases[(self.series, self.openstack)] |
169 | + |
170 | + def _get_openstack_release_string(self): |
171 | + """Get openstack release string. |
172 | + |
173 | + Return a string representing the openstack release. |
174 | + """ |
175 | + releases = OrderedDict([ |
176 | + ('precise', 'essex'), |
177 | + ('quantal', 'folsom'), |
178 | + ('raring', 'grizzly'), |
179 | + ('saucy', 'havana'), |
180 | + ('trusty', 'icehouse'), |
181 | + ('utopic', 'juno'), |
182 | + ('vivid', 'kilo'), |
183 | + ]) |
184 | + if self.openstack: |
185 | + os_origin = self.openstack.split(':')[1] |
186 | + return os_origin.split('%s-' % self.series)[1].split('/')[0] |
187 | + else: |
188 | + return releases[self.series] |
189 | |
190 | === modified file 'hooks/charmhelpers/contrib/openstack/context.py' |
191 | --- hooks/charmhelpers/contrib/openstack/context.py 2015-01-29 13:02:55 +0000 |
192 | +++ hooks/charmhelpers/contrib/openstack/context.py 2015-04-16 21:56:47 +0000 |
193 | @@ -16,11 +16,13 @@ |
194 | |
195 | import json |
196 | import os |
197 | +import re |
198 | import time |
199 | from base64 import b64decode |
200 | from subprocess import check_call |
201 | |
202 | import six |
203 | +import yaml |
204 | |
205 | from charmhelpers.fetch import ( |
206 | apt_install, |
207 | @@ -45,8 +47,11 @@ |
208 | ) |
209 | |
210 | from charmhelpers.core.sysctl import create as sysctl_create |
211 | +from charmhelpers.core.strutils import bool_from_string |
212 | |
213 | from charmhelpers.core.host import ( |
214 | + list_nics, |
215 | + get_nic_hwaddr, |
216 | mkdir, |
217 | write_file, |
218 | ) |
219 | @@ -63,16 +68,22 @@ |
220 | ) |
221 | from charmhelpers.contrib.openstack.neutron import ( |
222 | neutron_plugin_attribute, |
223 | + parse_data_port_mappings, |
224 | +) |
225 | +from charmhelpers.contrib.openstack.ip import ( |
226 | + resolve_address, |
227 | + INTERNAL, |
228 | ) |
229 | from charmhelpers.contrib.network.ip import ( |
230 | get_address_in_network, |
231 | + get_ipv4_addr, |
232 | get_ipv6_addr, |
233 | get_netmask_for_address, |
234 | format_ipv6_addr, |
235 | is_address_in_network, |
236 | + is_bridge_member, |
237 | ) |
238 | from charmhelpers.contrib.openstack.utils import get_host_ip |
239 | - |
240 | CA_CERT_PATH = '/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt' |
241 | ADDRESS_TYPES = ['admin', 'internal', 'public'] |
242 | |
243 | @@ -104,9 +115,41 @@ |
244 | def config_flags_parser(config_flags): |
245 | """Parses config flags string into dict. |
246 | |
247 | + This parsing method supports a few different formats for the config |
248 | + flag values to be parsed: |
249 | + |
250 | + 1. A string in the simple format of key=value pairs, with the possibility |
251 | + of specifying multiple key value pairs within the same string. For |
252 | + example, a string in the format of 'key1=value1, key2=value2' will |
253 | + return a dict of: |
254 | + {'key1': 'value1', |
255 | + 'key2': 'value2'}. |
256 | + |
257 | + 2. A string in the above format, but supporting a comma-delimited list |
258 | + of values for the same key. For example, a string in the format of |
259 | + 'key1=value1, key2=value3,value4,value5' will return a dict of: |
260 | + {'key1', 'value1', |
261 | + 'key2', 'value2,value3,value4'} |
262 | + |
263 | + 3. A string containing a colon character (:) prior to an equal |
264 | + character (=) will be treated as yaml and parsed as such. This can be |
265 | + used to specify more complex key value pairs. For example, |
266 | + a string in the format of 'key1: subkey1=value1, subkey2=value2' will |
267 | + return a dict of: |
268 | + {'key1', 'subkey1=value1, subkey2=value2'} |
269 | + |
270 | The provided config_flags string may be a list of comma-separated values |
271 | which themselves may be comma-separated list of values. |
272 | """ |
273 | + # If we find a colon before an equals sign then treat it as yaml. |
274 | + # Note: limit it to finding the colon first since this indicates assignment |
275 | + # for inline yaml. |
276 | + colon = config_flags.find(':') |
277 | + equals = config_flags.find('=') |
278 | + if colon > 0: |
279 | + if colon < equals or equals < 0: |
280 | + return yaml.safe_load(config_flags) |
281 | + |
282 | if config_flags.find('==') >= 0: |
283 | log("config_flags is not in expected format (key=value)", level=ERROR) |
284 | raise OSContextError |
285 | @@ -191,7 +234,7 @@ |
286 | unit=local_unit()) |
287 | if set_hostname != access_hostname: |
288 | relation_set(relation_settings={hostname_key: access_hostname}) |
289 | - return ctxt # Defer any further hook execution for now.... |
290 | + return None # Defer any further hook execution for now.... |
291 | |
292 | password_setting = 'password' |
293 | if self.relation_prefix: |
294 | @@ -277,12 +320,29 @@ |
295 | |
296 | |
297 | class IdentityServiceContext(OSContextGenerator): |
298 | - interfaces = ['identity-service'] |
299 | + |
300 | + def __init__(self, service=None, service_user=None, rel_name='identity-service'): |
301 | + self.service = service |
302 | + self.service_user = service_user |
303 | + self.rel_name = rel_name |
304 | + self.interfaces = [self.rel_name] |
305 | |
306 | def __call__(self): |
307 | - log('Generating template context for identity-service', level=DEBUG) |
308 | + log('Generating template context for ' + self.rel_name, level=DEBUG) |
309 | ctxt = {} |
310 | - for rid in relation_ids('identity-service'): |
311 | + |
312 | + if self.service and self.service_user: |
313 | + # This is required for pki token signing if we don't want /tmp to |
314 | + # be used. |
315 | + cachedir = '/var/cache/%s' % (self.service) |
316 | + if not os.path.isdir(cachedir): |
317 | + log("Creating service cache dir %s" % (cachedir), level=DEBUG) |
318 | + mkdir(path=cachedir, owner=self.service_user, |
319 | + group=self.service_user, perms=0o700) |
320 | + |
321 | + ctxt['signing_dir'] = cachedir |
322 | + |
323 | + for rid in relation_ids(self.rel_name): |
324 | for unit in related_units(rid): |
325 | rdata = relation_get(rid=rid, unit=unit) |
326 | serv_host = rdata.get('service_host') |
327 | @@ -291,15 +351,16 @@ |
328 | auth_host = format_ipv6_addr(auth_host) or auth_host |
329 | svc_protocol = rdata.get('service_protocol') or 'http' |
330 | auth_protocol = rdata.get('auth_protocol') or 'http' |
331 | - ctxt = {'service_port': rdata.get('service_port'), |
332 | - 'service_host': serv_host, |
333 | - 'auth_host': auth_host, |
334 | - 'auth_port': rdata.get('auth_port'), |
335 | - 'admin_tenant_name': rdata.get('service_tenant'), |
336 | - 'admin_user': rdata.get('service_username'), |
337 | - 'admin_password': rdata.get('service_password'), |
338 | - 'service_protocol': svc_protocol, |
339 | - 'auth_protocol': auth_protocol} |
340 | + ctxt.update({'service_port': rdata.get('service_port'), |
341 | + 'service_host': serv_host, |
342 | + 'auth_host': auth_host, |
343 | + 'auth_port': rdata.get('auth_port'), |
344 | + 'admin_tenant_name': rdata.get('service_tenant'), |
345 | + 'admin_user': rdata.get('service_username'), |
346 | + 'admin_password': rdata.get('service_password'), |
347 | + 'service_protocol': svc_protocol, |
348 | + 'auth_protocol': auth_protocol}) |
349 | + |
350 | if context_complete(ctxt): |
351 | # NOTE(jamespage) this is required for >= icehouse |
352 | # so a missing value just indicates keystone needs |
353 | @@ -398,6 +459,11 @@ |
354 | |
355 | ctxt['rabbitmq_hosts'] = ','.join(sorted(rabbitmq_hosts)) |
356 | |
357 | + oslo_messaging_flags = conf.get('oslo-messaging-flags', None) |
358 | + if oslo_messaging_flags: |
359 | + ctxt['oslo_messaging_flags'] = config_flags_parser( |
360 | + oslo_messaging_flags) |
361 | + |
362 | if not context_complete(ctxt): |
363 | return {} |
364 | |
365 | @@ -677,7 +743,14 @@ |
366 | 'endpoints': [], |
367 | 'ext_ports': []} |
368 | |
369 | - for cn in self.canonical_names(): |
370 | + cns = self.canonical_names() |
371 | + if cns: |
372 | + for cn in cns: |
373 | + self.configure_cert(cn) |
374 | + else: |
375 | + # Expect cert/key provided in config (currently assumed that ca |
376 | + # uses ip for cn) |
377 | + cn = resolve_address(endpoint_type=INTERNAL) |
378 | self.configure_cert(cn) |
379 | |
380 | addresses = self.get_network_addresses() |
381 | @@ -740,6 +813,19 @@ |
382 | |
383 | return ovs_ctxt |
384 | |
385 | + def nuage_ctxt(self): |
386 | + driver = neutron_plugin_attribute(self.plugin, 'driver', |
387 | + self.network_manager) |
388 | + config = neutron_plugin_attribute(self.plugin, 'config', |
389 | + self.network_manager) |
390 | + nuage_ctxt = {'core_plugin': driver, |
391 | + 'neutron_plugin': 'vsp', |
392 | + 'neutron_security_groups': self.neutron_security_groups, |
393 | + 'local_ip': unit_private_ip(), |
394 | + 'config': config} |
395 | + |
396 | + return nuage_ctxt |
397 | + |
398 | def nvp_ctxt(self): |
399 | driver = neutron_plugin_attribute(self.plugin, 'driver', |
400 | self.network_manager) |
401 | @@ -823,6 +909,8 @@ |
402 | ctxt.update(self.n1kv_ctxt()) |
403 | elif self.plugin == 'Calico': |
404 | ctxt.update(self.calico_ctxt()) |
405 | + elif self.plugin == 'vsp': |
406 | + ctxt.update(self.nuage_ctxt()) |
407 | |
408 | alchemy_flags = config('neutron-alchemy-flags') |
409 | if alchemy_flags: |
410 | @@ -833,6 +921,48 @@ |
411 | return ctxt |
412 | |
413 | |
414 | +class NeutronPortContext(OSContextGenerator): |
415 | + NIC_PREFIXES = ['eth', 'bond'] |
416 | + |
417 | + def resolve_ports(self, ports): |
418 | + """Resolve NICs not yet bound to bridge(s) |
419 | + |
420 | + If hwaddress provided then returns resolved hwaddress otherwise NIC. |
421 | + """ |
422 | + if not ports: |
423 | + return None |
424 | + |
425 | + hwaddr_to_nic = {} |
426 | + hwaddr_to_ip = {} |
427 | + for nic in list_nics(self.NIC_PREFIXES): |
428 | + hwaddr = get_nic_hwaddr(nic) |
429 | + hwaddr_to_nic[hwaddr] = nic |
430 | + addresses = get_ipv4_addr(nic, fatal=False) |
431 | + addresses += get_ipv6_addr(iface=nic, fatal=False) |
432 | + hwaddr_to_ip[hwaddr] = addresses |
433 | + |
434 | + resolved = [] |
435 | + mac_regex = re.compile(r'([0-9A-F]{2}[:-]){5}([0-9A-F]{2})', re.I) |
436 | + for entry in ports: |
437 | + if re.match(mac_regex, entry): |
438 | + # NIC is in known NICs and does NOT hace an IP address |
439 | + if entry in hwaddr_to_nic and not hwaddr_to_ip[entry]: |
440 | + # If the nic is part of a bridge then don't use it |
441 | + if is_bridge_member(hwaddr_to_nic[entry]): |
442 | + continue |
443 | + |
444 | + # Entry is a MAC address for a valid interface that doesn't |
445 | + # have an IP address assigned yet. |
446 | + resolved.append(hwaddr_to_nic[entry]) |
447 | + else: |
448 | + # If the passed entry is not a MAC address, assume it's a valid |
449 | + # interface, and that the user put it there on purpose (we can |
450 | + # trust it to be the real external network). |
451 | + resolved.append(entry) |
452 | + |
453 | + return resolved |
454 | + |
455 | + |
456 | class OSConfigFlagContext(OSContextGenerator): |
457 | """Provides support for user-defined config flags. |
458 | |
459 | @@ -1021,6 +1151,8 @@ |
460 | for unit in related_units(rid): |
461 | ctxt['zmq_nonce'] = relation_get('nonce', unit, rid) |
462 | ctxt['zmq_host'] = relation_get('host', unit, rid) |
463 | + ctxt['zmq_redis_address'] = relation_get( |
464 | + 'zmq_redis_address', unit, rid) |
465 | |
466 | return ctxt |
467 | |
468 | @@ -1052,3 +1184,145 @@ |
469 | sysctl_create(sysctl_dict, |
470 | '/etc/sysctl.d/50-{0}.conf'.format(charm_name())) |
471 | return {'sysctl': sysctl_dict} |
472 | + |
473 | + |
474 | +class NeutronAPIContext(OSContextGenerator): |
475 | + ''' |
476 | + Inspects current neutron-plugin-api relation for neutron settings. Return |
477 | + defaults if it is not present. |
478 | + ''' |
479 | + interfaces = ['neutron-plugin-api'] |
480 | + |
481 | + def __call__(self): |
482 | + self.neutron_defaults = { |
483 | + 'l2_population': { |
484 | + 'rel_key': 'l2-population', |
485 | + 'default': False, |
486 | + }, |
487 | + 'overlay_network_type': { |
488 | + 'rel_key': 'overlay-network-type', |
489 | + 'default': 'gre', |
490 | + }, |
491 | + 'neutron_security_groups': { |
492 | + 'rel_key': 'neutron-security-groups', |
493 | + 'default': False, |
494 | + }, |
495 | + 'network_device_mtu': { |
496 | + 'rel_key': 'network-device-mtu', |
497 | + 'default': None, |
498 | + }, |
499 | + 'enable_dvr': { |
500 | + 'rel_key': 'enable-dvr', |
501 | + 'default': False, |
502 | + }, |
503 | + 'enable_l3ha': { |
504 | + 'rel_key': 'enable-l3ha', |
505 | + 'default': False, |
506 | + }, |
507 | + } |
508 | + ctxt = self.get_neutron_options({}) |
509 | + for rid in relation_ids('neutron-plugin-api'): |
510 | + for unit in related_units(rid): |
511 | + rdata = relation_get(rid=rid, unit=unit) |
512 | + if 'l2-population' in rdata: |
513 | + ctxt.update(self.get_neutron_options(rdata)) |
514 | + |
515 | + return ctxt |
516 | + |
517 | + def get_neutron_options(self, rdata): |
518 | + settings = {} |
519 | + for nkey in self.neutron_defaults.keys(): |
520 | + defv = self.neutron_defaults[nkey]['default'] |
521 | + rkey = self.neutron_defaults[nkey]['rel_key'] |
522 | + if rkey in rdata.keys(): |
523 | + if type(defv) is bool: |
524 | + settings[nkey] = bool_from_string(rdata[rkey]) |
525 | + else: |
526 | + settings[nkey] = rdata[rkey] |
527 | + else: |
528 | + settings[nkey] = defv |
529 | + return settings |
530 | + |
531 | + |
532 | +class ExternalPortContext(NeutronPortContext): |
533 | + |
534 | + def __call__(self): |
535 | + ctxt = {} |
536 | + ports = config('ext-port') |
537 | + if ports: |
538 | + ports = [p.strip() for p in ports.split()] |
539 | + ports = self.resolve_ports(ports) |
540 | + if ports: |
541 | + ctxt = {"ext_port": ports[0]} |
542 | + napi_settings = NeutronAPIContext()() |
543 | + mtu = napi_settings.get('network_device_mtu') |
544 | + if mtu: |
545 | + ctxt['ext_port_mtu'] = mtu |
546 | + |
547 | + return ctxt |
548 | + |
549 | + |
550 | +class DataPortContext(NeutronPortContext): |
551 | + |
552 | + def __call__(self): |
553 | + ports = config('data-port') |
554 | + if ports: |
555 | + portmap = parse_data_port_mappings(ports) |
556 | + ports = portmap.values() |
557 | + resolved = self.resolve_ports(ports) |
558 | + normalized = {get_nic_hwaddr(port): port for port in resolved |
559 | + if port not in ports} |
560 | + normalized.update({port: port for port in resolved |
561 | + if port in ports}) |
562 | + if resolved: |
563 | + return {bridge: normalized[port] for bridge, port in |
564 | + six.iteritems(portmap) if port in normalized.keys()} |
565 | + |
566 | + return None |
567 | + |
568 | + |
569 | +class PhyNICMTUContext(DataPortContext): |
570 | + |
571 | + def __call__(self): |
572 | + ctxt = {} |
573 | + mappings = super(PhyNICMTUContext, self).__call__() |
574 | + if mappings and mappings.values(): |
575 | + ports = mappings.values() |
576 | + napi_settings = NeutronAPIContext()() |
577 | + mtu = napi_settings.get('network_device_mtu') |
578 | + if mtu: |
579 | + ctxt["devs"] = '\\n'.join(ports) |
580 | + ctxt['mtu'] = mtu |
581 | + |
582 | + return ctxt |
583 | + |
584 | + |
585 | +class NetworkServiceContext(OSContextGenerator): |
586 | + |
587 | + def __init__(self, rel_name='quantum-network-service'): |
588 | + self.rel_name = rel_name |
589 | + self.interfaces = [rel_name] |
590 | + |
591 | + def __call__(self): |
592 | + for rid in relation_ids(self.rel_name): |
593 | + for unit in related_units(rid): |
594 | + rdata = relation_get(rid=rid, unit=unit) |
595 | + ctxt = { |
596 | + 'keystone_host': rdata.get('keystone_host'), |
597 | + 'service_port': rdata.get('service_port'), |
598 | + 'auth_port': rdata.get('auth_port'), |
599 | + 'service_tenant': rdata.get('service_tenant'), |
600 | + 'service_username': rdata.get('service_username'), |
601 | + 'service_password': rdata.get('service_password'), |
602 | + 'quantum_host': rdata.get('quantum_host'), |
603 | + 'quantum_port': rdata.get('quantum_port'), |
604 | + 'quantum_url': rdata.get('quantum_url'), |
605 | + 'region': rdata.get('region'), |
606 | + 'service_protocol': |
607 | + rdata.get('service_protocol') or 'http', |
608 | + 'auth_protocol': |
609 | + rdata.get('auth_protocol') or 'http', |
610 | + } |
611 | + if context_complete(ctxt): |
612 | + return ctxt |
613 | + return {} |
614 | |
615 | === added directory 'hooks/charmhelpers/contrib/openstack/files' |
616 | === added file 'hooks/charmhelpers/contrib/openstack/files/__init__.py' |
617 | --- hooks/charmhelpers/contrib/openstack/files/__init__.py 1970-01-01 00:00:00 +0000 |
618 | +++ hooks/charmhelpers/contrib/openstack/files/__init__.py 2015-04-16 21:56:47 +0000 |
619 | @@ -0,0 +1,18 @@ |
620 | +# Copyright 2014-2015 Canonical Limited. |
621 | +# |
622 | +# This file is part of charm-helpers. |
623 | +# |
624 | +# charm-helpers is free software: you can redistribute it and/or modify |
625 | +# it under the terms of the GNU Lesser General Public License version 3 as |
626 | +# published by the Free Software Foundation. |
627 | +# |
628 | +# charm-helpers is distributed in the hope that it will be useful, |
629 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
630 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
631 | +# GNU Lesser General Public License for more details. |
632 | +# |
633 | +# You should have received a copy of the GNU Lesser General Public License |
634 | +# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
635 | + |
636 | +# dummy __init__.py to fool syncer into thinking this is a syncable python |
637 | +# module |
638 | |
639 | === modified file 'hooks/charmhelpers/contrib/openstack/ip.py' |
640 | --- hooks/charmhelpers/contrib/openstack/ip.py 2015-01-29 13:02:55 +0000 |
641 | +++ hooks/charmhelpers/contrib/openstack/ip.py 2015-04-16 21:56:47 +0000 |
642 | @@ -26,6 +26,8 @@ |
643 | ) |
644 | from charmhelpers.contrib.hahelpers.cluster import is_clustered |
645 | |
646 | +from functools import partial |
647 | + |
648 | PUBLIC = 'public' |
649 | INTERNAL = 'int' |
650 | ADMIN = 'admin' |
651 | @@ -107,3 +109,38 @@ |
652 | "clustered=%s)" % (net_type, clustered)) |
653 | |
654 | return resolved_address |
655 | + |
656 | + |
657 | +def endpoint_url(configs, url_template, port, endpoint_type=PUBLIC, |
658 | + override=None): |
659 | + """Returns the correct endpoint URL to advertise to Keystone. |
660 | + |
661 | + This method provides the correct endpoint URL which should be advertised to |
662 | + the keystone charm for endpoint creation. This method allows for the url to |
663 | + be overridden to force a keystone endpoint to have specific URL for any of |
664 | + the defined scopes (admin, internal, public). |
665 | + |
666 | + :param configs: OSTemplateRenderer config templating object to inspect |
667 | + for a complete https context. |
668 | + :param url_template: str format string for creating the url template. Only |
669 | + two values will be passed - the scheme+hostname |
670 | + returned by the canonical_url and the port. |
671 | + :param endpoint_type: str endpoint type to resolve. |
672 | + :param override: str the name of the config option which overrides the |
673 | + endpoint URL defined by the charm itself. None will |
674 | + disable any overrides (default). |
675 | + """ |
676 | + if override: |
677 | + # Return any user-defined overrides for the keystone endpoint URL. |
678 | + user_value = config(override) |
679 | + if user_value: |
680 | + return user_value.strip() |
681 | + |
682 | + return url_template % (canonical_url(configs, endpoint_type), port) |
683 | + |
684 | + |
685 | +public_endpoint = partial(endpoint_url, endpoint_type=PUBLIC) |
686 | + |
687 | +internal_endpoint = partial(endpoint_url, endpoint_type=INTERNAL) |
688 | + |
689 | +admin_endpoint = partial(endpoint_url, endpoint_type=ADMIN) |
690 | |
691 | === modified file 'hooks/charmhelpers/contrib/openstack/neutron.py' |
692 | --- hooks/charmhelpers/contrib/openstack/neutron.py 2015-01-29 13:02:55 +0000 |
693 | +++ hooks/charmhelpers/contrib/openstack/neutron.py 2015-04-16 21:56:47 +0000 |
694 | @@ -16,6 +16,7 @@ |
695 | |
696 | # Various utilies for dealing with Neutron and the renaming from Quantum. |
697 | |
698 | +import six |
699 | from subprocess import check_output |
700 | |
701 | from charmhelpers.core.hookenv import ( |
702 | @@ -179,6 +180,19 @@ |
703 | 'nova-api-metadata']], |
704 | 'server_packages': ['neutron-server', 'calico-control'], |
705 | 'server_services': ['neutron-server'] |
706 | + }, |
707 | + 'vsp': { |
708 | + 'config': '/etc/neutron/plugins/nuage/nuage_plugin.ini', |
709 | + 'driver': 'neutron.plugins.nuage.plugin.NuagePlugin', |
710 | + 'contexts': [ |
711 | + context.SharedDBContext(user=config('neutron-database-user'), |
712 | + database=config('neutron-database'), |
713 | + relation_prefix='neutron', |
714 | + ssl_dir=NEUTRON_CONF_DIR)], |
715 | + 'services': [], |
716 | + 'packages': [], |
717 | + 'server_packages': ['neutron-server', 'neutron-plugin-nuage'], |
718 | + 'server_services': ['neutron-server'] |
719 | } |
720 | } |
721 | if release >= 'icehouse': |
722 | @@ -237,3 +251,72 @@ |
723 | else: |
724 | # ensure accurate naming for all releases post-H |
725 | return 'neutron' |
726 | + |
727 | + |
728 | +def parse_mappings(mappings): |
729 | + parsed = {} |
730 | + if mappings: |
731 | + mappings = mappings.split(' ') |
732 | + for m in mappings: |
733 | + p = m.partition(':') |
734 | + if p[1] == ':': |
735 | + parsed[p[0].strip()] = p[2].strip() |
736 | + |
737 | + return parsed |
738 | + |
739 | + |
740 | +def parse_bridge_mappings(mappings): |
741 | + """Parse bridge mappings. |
742 | + |
743 | + Mappings must be a space-delimited list of provider:bridge mappings. |
744 | + |
745 | + Returns dict of the form {provider:bridge}. |
746 | + """ |
747 | + return parse_mappings(mappings) |
748 | + |
749 | + |
750 | +def parse_data_port_mappings(mappings, default_bridge='br-data'): |
751 | + """Parse data port mappings. |
752 | + |
753 | + Mappings must be a space-delimited list of bridge:port mappings. |
754 | + |
755 | + Returns dict of the form {bridge:port}. |
756 | + """ |
757 | + _mappings = parse_mappings(mappings) |
758 | + if not _mappings: |
759 | + if not mappings: |
760 | + return {} |
761 | + |
762 | + # For backwards-compatibility we need to support port-only provided in |
763 | + # config. |
764 | + _mappings = {default_bridge: mappings.split(' ')[0]} |
765 | + |
766 | + bridges = _mappings.keys() |
767 | + ports = _mappings.values() |
768 | + if len(set(bridges)) != len(bridges): |
769 | + raise Exception("It is not allowed to have more than one port " |
770 | + "configured on the same bridge") |
771 | + |
772 | + if len(set(ports)) != len(ports): |
773 | + raise Exception("It is not allowed to have the same port configured " |
774 | + "on more than one bridge") |
775 | + |
776 | + return _mappings |
777 | + |
778 | + |
779 | +def parse_vlan_range_mappings(mappings): |
780 | + """Parse vlan range mappings. |
781 | + |
782 | + Mappings must be a space-delimited list of provider:start:end mappings. |
783 | + |
784 | + Returns dict of the form {provider: (start, end)}. |
785 | + """ |
786 | + _mappings = parse_mappings(mappings) |
787 | + if not _mappings: |
788 | + return {} |
789 | + |
790 | + mappings = {} |
791 | + for p, r in six.iteritems(_mappings): |
792 | + mappings[p] = tuple(r.split(':')) |
793 | + |
794 | + return mappings |
795 | |
796 | === modified file 'hooks/charmhelpers/contrib/openstack/utils.py' |
797 | --- hooks/charmhelpers/contrib/openstack/utils.py 2015-01-29 13:02:55 +0000 |
798 | +++ hooks/charmhelpers/contrib/openstack/utils.py 2015-04-16 21:56:47 +0000 |
799 | @@ -23,12 +23,17 @@ |
800 | import subprocess |
801 | import json |
802 | import os |
803 | -import socket |
804 | import sys |
805 | |
806 | import six |
807 | import yaml |
808 | |
809 | +from charmhelpers.contrib.network import ip |
810 | + |
811 | +from charmhelpers.core import ( |
812 | + unitdata, |
813 | +) |
814 | + |
815 | from charmhelpers.core.hookenv import ( |
816 | config, |
817 | log as juju_log, |
818 | @@ -103,6 +108,7 @@ |
819 | ('2.1.0', 'juno'), |
820 | ('2.2.0', 'juno'), |
821 | ('2.2.1', 'kilo'), |
822 | + ('2.2.2', 'kilo'), |
823 | ]) |
824 | |
825 | DEFAULT_LOOPBACK_SIZE = '5G' |
826 | @@ -328,6 +334,21 @@ |
827 | error_out("Invalid openstack-release specified: %s" % rel) |
828 | |
829 | |
830 | +def config_value_changed(option): |
831 | + """ |
832 | + Determine if config value changed since last call to this function. |
833 | + """ |
834 | + hook_data = unitdata.HookData() |
835 | + with hook_data(): |
836 | + db = unitdata.kv() |
837 | + current = config(option) |
838 | + saved = db.get(option) |
839 | + db.set(option, current) |
840 | + if saved is None: |
841 | + return False |
842 | + return current != saved |
843 | + |
844 | + |
845 | def save_script_rc(script_path="scripts/scriptrc", **env_vars): |
846 | """ |
847 | Write an rc file in the charm-delivered directory containing |
848 | @@ -420,77 +441,10 @@ |
849 | else: |
850 | zap_disk(block_device) |
851 | |
852 | - |
853 | -def is_ip(address): |
854 | - """ |
855 | - Returns True if address is a valid IP address. |
856 | - """ |
857 | - try: |
858 | - # Test to see if already an IPv4 address |
859 | - socket.inet_aton(address) |
860 | - return True |
861 | - except socket.error: |
862 | - return False |
863 | - |
864 | - |
865 | -def ns_query(address): |
866 | - try: |
867 | - import dns.resolver |
868 | - except ImportError: |
869 | - apt_install('python-dnspython') |
870 | - import dns.resolver |
871 | - |
872 | - if isinstance(address, dns.name.Name): |
873 | - rtype = 'PTR' |
874 | - elif isinstance(address, six.string_types): |
875 | - rtype = 'A' |
876 | - else: |
877 | - return None |
878 | - |
879 | - answers = dns.resolver.query(address, rtype) |
880 | - if answers: |
881 | - return str(answers[0]) |
882 | - return None |
883 | - |
884 | - |
885 | -def get_host_ip(hostname): |
886 | - """ |
887 | - Resolves the IP for a given hostname, or returns |
888 | - the input if it is already an IP. |
889 | - """ |
890 | - if is_ip(hostname): |
891 | - return hostname |
892 | - |
893 | - return ns_query(hostname) |
894 | - |
895 | - |
896 | -def get_hostname(address, fqdn=True): |
897 | - """ |
898 | - Resolves hostname for given IP, or returns the input |
899 | - if it is already a hostname. |
900 | - """ |
901 | - if is_ip(address): |
902 | - try: |
903 | - import dns.reversename |
904 | - except ImportError: |
905 | - apt_install('python-dnspython') |
906 | - import dns.reversename |
907 | - |
908 | - rev = dns.reversename.from_address(address) |
909 | - result = ns_query(rev) |
910 | - if not result: |
911 | - return None |
912 | - else: |
913 | - result = address |
914 | - |
915 | - if fqdn: |
916 | - # strip trailing . |
917 | - if result.endswith('.'): |
918 | - return result[:-1] |
919 | - else: |
920 | - return result |
921 | - else: |
922 | - return result.split('.')[0] |
923 | +is_ip = ip.is_ip |
924 | +ns_query = ip.ns_query |
925 | +get_host_ip = ip.get_host_ip |
926 | +get_hostname = ip.get_hostname |
927 | |
928 | |
929 | def get_matchmaker_map(mm_file='/etc/oslo/matchmaker_ring.json'): |
930 | @@ -534,82 +488,106 @@ |
931 | |
932 | |
933 | def git_install_requested(): |
934 | - """Returns true if openstack-origin-git is specified.""" |
935 | - return config('openstack-origin-git') != "None" |
936 | + """ |
937 | + Returns true if openstack-origin-git is specified. |
938 | + """ |
939 | + return config('openstack-origin-git') is not None |
940 | |
941 | |
942 | requirements_dir = None |
943 | |
944 | |
945 | -def git_clone_and_install(file_name, core_project): |
946 | - """Clone/install all OpenStack repos specified in yaml config file.""" |
947 | +def git_clone_and_install(projects_yaml, core_project): |
948 | + """ |
949 | + Clone/install all specified OpenStack repositories. |
950 | + |
951 | + The expected format of projects_yaml is: |
952 | + repositories: |
953 | + - {name: keystone, |
954 | + repository: 'git://git.openstack.org/openstack/keystone.git', |
955 | + branch: 'stable/icehouse'} |
956 | + - {name: requirements, |
957 | + repository: 'git://git.openstack.org/openstack/requirements.git', |
958 | + branch: 'stable/icehouse'} |
959 | + directory: /mnt/openstack-git |
960 | + http_proxy: http://squid.internal:3128 |
961 | + https_proxy: https://squid.internal:3128 |
962 | + |
963 | + The directory, http_proxy, and https_proxy keys are optional. |
964 | + """ |
965 | global requirements_dir |
966 | + parent_dir = '/mnt/openstack-git' |
967 | |
968 | - if file_name == "None": |
969 | + if not projects_yaml: |
970 | return |
971 | |
972 | - yaml_file = os.path.join(charm_dir(), file_name) |
973 | - |
974 | - # clone/install the requirements project first |
975 | - installed = _git_clone_and_install_subset(yaml_file, |
976 | - whitelist=['requirements']) |
977 | - if 'requirements' not in installed: |
978 | - error_out('requirements git repository must be specified') |
979 | - |
980 | - # clone/install all other projects except requirements and the core project |
981 | - blacklist = ['requirements', core_project] |
982 | - _git_clone_and_install_subset(yaml_file, blacklist=blacklist, |
983 | - update_requirements=True) |
984 | - |
985 | - # clone/install the core project |
986 | - whitelist = [core_project] |
987 | - installed = _git_clone_and_install_subset(yaml_file, whitelist=whitelist, |
988 | - update_requirements=True) |
989 | - if core_project not in installed: |
990 | - error_out('{} git repository must be specified'.format(core_project)) |
991 | - |
992 | - |
993 | -def _git_clone_and_install_subset(yaml_file, whitelist=[], blacklist=[], |
994 | - update_requirements=False): |
995 | - """Clone/install subset of OpenStack repos specified in yaml config file.""" |
996 | - global requirements_dir |
997 | - installed = [] |
998 | - |
999 | - with open(yaml_file, 'r') as fd: |
1000 | - projects = yaml.load(fd) |
1001 | - for proj, val in projects.items(): |
1002 | - # The project subset is chosen based on the following 3 rules: |
1003 | - # 1) If project is in blacklist, we don't clone/install it, period. |
1004 | - # 2) If whitelist is empty, we clone/install everything else. |
1005 | - # 3) If whitelist is not empty, we clone/install everything in the |
1006 | - # whitelist. |
1007 | - if proj in blacklist: |
1008 | - continue |
1009 | - if whitelist and proj not in whitelist: |
1010 | - continue |
1011 | - repo = val['repository'] |
1012 | - branch = val['branch'] |
1013 | - repo_dir = _git_clone_and_install_single(repo, branch, |
1014 | - update_requirements) |
1015 | - if proj == 'requirements': |
1016 | - requirements_dir = repo_dir |
1017 | - installed.append(proj) |
1018 | - return installed |
1019 | - |
1020 | - |
1021 | -def _git_clone_and_install_single(repo, branch, update_requirements=False): |
1022 | - """Clone and install a single git repository.""" |
1023 | - dest_parent_dir = "/mnt/openstack-git/" |
1024 | - dest_dir = os.path.join(dest_parent_dir, os.path.basename(repo)) |
1025 | - |
1026 | - if not os.path.exists(dest_parent_dir): |
1027 | - juju_log('Host dir not mounted at {}. ' |
1028 | - 'Creating directory there instead.'.format(dest_parent_dir)) |
1029 | - os.mkdir(dest_parent_dir) |
1030 | + projects = yaml.load(projects_yaml) |
1031 | + _git_validate_projects_yaml(projects, core_project) |
1032 | + |
1033 | + old_environ = dict(os.environ) |
1034 | + |
1035 | + if 'http_proxy' in projects.keys(): |
1036 | + os.environ['http_proxy'] = projects['http_proxy'] |
1037 | + if 'https_proxy' in projects.keys(): |
1038 | + os.environ['https_proxy'] = projects['https_proxy'] |
1039 | + |
1040 | + if 'directory' in projects.keys(): |
1041 | + parent_dir = projects['directory'] |
1042 | + |
1043 | + for p in projects['repositories']: |
1044 | + repo = p['repository'] |
1045 | + branch = p['branch'] |
1046 | + if p['name'] == 'requirements': |
1047 | + repo_dir = _git_clone_and_install_single(repo, branch, parent_dir, |
1048 | + update_requirements=False) |
1049 | + requirements_dir = repo_dir |
1050 | + else: |
1051 | + repo_dir = _git_clone_and_install_single(repo, branch, parent_dir, |
1052 | + update_requirements=True) |
1053 | + |
1054 | + os.environ = old_environ |
1055 | + |
1056 | + |
1057 | +def _git_validate_projects_yaml(projects, core_project): |
1058 | + """ |
1059 | + Validate the projects yaml. |
1060 | + """ |
1061 | + _git_ensure_key_exists('repositories', projects) |
1062 | + |
1063 | + for project in projects['repositories']: |
1064 | + _git_ensure_key_exists('name', project.keys()) |
1065 | + _git_ensure_key_exists('repository', project.keys()) |
1066 | + _git_ensure_key_exists('branch', project.keys()) |
1067 | + |
1068 | + if projects['repositories'][0]['name'] != 'requirements': |
1069 | + error_out('{} git repo must be specified first'.format('requirements')) |
1070 | + |
1071 | + if projects['repositories'][-1]['name'] != core_project: |
1072 | + error_out('{} git repo must be specified last'.format(core_project)) |
1073 | + |
1074 | + |
1075 | +def _git_ensure_key_exists(key, keys): |
1076 | + """ |
1077 | + Ensure that key exists in keys. |
1078 | + """ |
1079 | + if key not in keys: |
1080 | + error_out('openstack-origin-git key \'{}\' is missing'.format(key)) |
1081 | + |
1082 | + |
1083 | +def _git_clone_and_install_single(repo, branch, parent_dir, update_requirements): |
1084 | + """ |
1085 | + Clone and install a single git repository. |
1086 | + """ |
1087 | + dest_dir = os.path.join(parent_dir, os.path.basename(repo)) |
1088 | + |
1089 | + if not os.path.exists(parent_dir): |
1090 | + juju_log('Directory already exists at {}. ' |
1091 | + 'No need to create directory.'.format(parent_dir)) |
1092 | + os.mkdir(parent_dir) |
1093 | |
1094 | if not os.path.exists(dest_dir): |
1095 | juju_log('Cloning git repo: {}, branch: {}'.format(repo, branch)) |
1096 | - repo_dir = install_remote(repo, dest=dest_parent_dir, branch=branch) |
1097 | + repo_dir = install_remote(repo, dest=parent_dir, branch=branch) |
1098 | else: |
1099 | repo_dir = dest_dir |
1100 | |
1101 | @@ -626,16 +604,39 @@ |
1102 | |
1103 | |
1104 | def _git_update_requirements(package_dir, reqs_dir): |
1105 | - """Update from global requirements. |
1106 | + """ |
1107 | + Update from global requirements. |
1108 | |
1109 | - Update an OpenStack git directory's requirements.txt and |
1110 | - test-requirements.txt from global-requirements.txt.""" |
1111 | + Update an OpenStack git directory's requirements.txt and |
1112 | + test-requirements.txt from global-requirements.txt. |
1113 | + """ |
1114 | orig_dir = os.getcwd() |
1115 | os.chdir(reqs_dir) |
1116 | - cmd = "python update.py {}".format(package_dir) |
1117 | + cmd = ['python', 'update.py', package_dir] |
1118 | try: |
1119 | - subprocess.check_call(cmd.split(' ')) |
1120 | + subprocess.check_call(cmd) |
1121 | except subprocess.CalledProcessError: |
1122 | package = os.path.basename(package_dir) |
1123 | error_out("Error updating {} from global-requirements.txt".format(package)) |
1124 | os.chdir(orig_dir) |
1125 | + |
1126 | + |
1127 | +def git_src_dir(projects_yaml, project): |
1128 | + """ |
1129 | + Return the directory where the specified project's source is located. |
1130 | + """ |
1131 | + parent_dir = '/mnt/openstack-git' |
1132 | + |
1133 | + if not projects_yaml: |
1134 | + return |
1135 | + |
1136 | + projects = yaml.load(projects_yaml) |
1137 | + |
1138 | + if 'directory' in projects.keys(): |
1139 | + parent_dir = projects['directory'] |
1140 | + |
1141 | + for p in projects['repositories']: |
1142 | + if p['name'] == project: |
1143 | + return os.path.join(parent_dir, os.path.basename(p['repository'])) |
1144 | + |
1145 | + return None |
1146 | |
1147 | === modified file 'hooks/charmhelpers/contrib/python/packages.py' |
1148 | --- hooks/charmhelpers/contrib/python/packages.py 2015-01-29 13:15:20 +0000 |
1149 | +++ hooks/charmhelpers/contrib/python/packages.py 2015-04-16 21:56:47 +0000 |
1150 | @@ -17,8 +17,6 @@ |
1151 | # You should have received a copy of the GNU Lesser General Public License |
1152 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
1153 | |
1154 | -__author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>" |
1155 | - |
1156 | from charmhelpers.fetch import apt_install, apt_update |
1157 | from charmhelpers.core.hookenv import log |
1158 | |
1159 | @@ -29,6 +27,8 @@ |
1160 | apt_install('python-pip') |
1161 | from pip import main as pip_execute |
1162 | |
1163 | +__author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>" |
1164 | + |
1165 | |
1166 | def parse_options(given, available): |
1167 | """Given a set of options, check if available""" |
1168 | |
1169 | === modified file 'hooks/charmhelpers/core/fstab.py' |
1170 | --- hooks/charmhelpers/core/fstab.py 2015-01-29 13:02:55 +0000 |
1171 | +++ hooks/charmhelpers/core/fstab.py 2015-04-16 21:56:47 +0000 |
1172 | @@ -17,11 +17,11 @@ |
1173 | # You should have received a copy of the GNU Lesser General Public License |
1174 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
1175 | |
1176 | -__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>' |
1177 | - |
1178 | import io |
1179 | import os |
1180 | |
1181 | +__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>' |
1182 | + |
1183 | |
1184 | class Fstab(io.FileIO): |
1185 | """This class extends file in order to implement a file reader/writer |
1186 | @@ -77,7 +77,7 @@ |
1187 | for line in self.readlines(): |
1188 | line = line.decode('us-ascii') |
1189 | try: |
1190 | - if line.strip() and not line.startswith("#"): |
1191 | + if line.strip() and not line.strip().startswith("#"): |
1192 | yield self._hydrate_entry(line) |
1193 | except ValueError: |
1194 | pass |
1195 | @@ -104,7 +104,7 @@ |
1196 | |
1197 | found = False |
1198 | for index, line in enumerate(lines): |
1199 | - if not line.startswith("#"): |
1200 | + if line.strip() and not line.strip().startswith("#"): |
1201 | if self._hydrate_entry(line) == entry: |
1202 | found = True |
1203 | break |
1204 | |
1205 | === modified file 'hooks/charmhelpers/core/hookenv.py' |
1206 | --- hooks/charmhelpers/core/hookenv.py 2015-01-29 13:02:55 +0000 |
1207 | +++ hooks/charmhelpers/core/hookenv.py 2015-04-16 21:56:47 +0000 |
1208 | @@ -20,11 +20,13 @@ |
1209 | # Authors: |
1210 | # Charm Helpers Developers <juju@lists.ubuntu.com> |
1211 | |
1212 | +from __future__ import print_function |
1213 | import os |
1214 | import json |
1215 | import yaml |
1216 | import subprocess |
1217 | import sys |
1218 | +import errno |
1219 | from subprocess import CalledProcessError |
1220 | |
1221 | import six |
1222 | @@ -87,7 +89,18 @@ |
1223 | if not isinstance(message, six.string_types): |
1224 | message = repr(message) |
1225 | command += [message] |
1226 | - subprocess.call(command) |
1227 | + # Missing juju-log should not cause failures in unit tests |
1228 | + # Send log output to stderr |
1229 | + try: |
1230 | + subprocess.call(command) |
1231 | + except OSError as e: |
1232 | + if e.errno == errno.ENOENT: |
1233 | + if level: |
1234 | + message = "{}: {}".format(level, message) |
1235 | + message = "juju-log: {}".format(message) |
1236 | + print(message, file=sys.stderr) |
1237 | + else: |
1238 | + raise |
1239 | |
1240 | |
1241 | class Serializable(UserDict): |
1242 | @@ -566,3 +579,29 @@ |
1243 | def charm_dir(): |
1244 | """Return the root directory of the current charm""" |
1245 | return os.environ.get('CHARM_DIR') |
1246 | + |
1247 | + |
1248 | +@cached |
1249 | +def action_get(key=None): |
1250 | + """Gets the value of an action parameter, or all key/value param pairs""" |
1251 | + cmd = ['action-get'] |
1252 | + if key is not None: |
1253 | + cmd.append(key) |
1254 | + cmd.append('--format=json') |
1255 | + action_data = json.loads(subprocess.check_output(cmd).decode('UTF-8')) |
1256 | + return action_data |
1257 | + |
1258 | + |
1259 | +def action_set(values): |
1260 | + """Sets the values to be returned after the action finishes""" |
1261 | + cmd = ['action-set'] |
1262 | + for k, v in list(values.items()): |
1263 | + cmd.append('{}={}'.format(k, v)) |
1264 | + subprocess.check_call(cmd) |
1265 | + |
1266 | + |
1267 | +def action_fail(message): |
1268 | + """Sets the action status to failed and sets the error message. |
1269 | + |
1270 | + The results set by action_set are preserved.""" |
1271 | + subprocess.check_call(['action-fail', message]) |
1272 | |
1273 | === modified file 'hooks/charmhelpers/core/host.py' |
1274 | --- hooks/charmhelpers/core/host.py 2015-01-29 13:02:55 +0000 |
1275 | +++ hooks/charmhelpers/core/host.py 2015-04-16 21:56:47 +0000 |
1276 | @@ -191,11 +191,11 @@ |
1277 | |
1278 | |
1279 | def write_file(path, content, owner='root', group='root', perms=0o444): |
1280 | - """Create or overwrite a file with the contents of a string""" |
1281 | + """Create or overwrite a file with the contents of a byte string.""" |
1282 | log("Writing file {} {}:{} {:o}".format(path, owner, group, perms)) |
1283 | uid = pwd.getpwnam(owner).pw_uid |
1284 | gid = grp.getgrnam(group).gr_gid |
1285 | - with open(path, 'w') as target: |
1286 | + with open(path, 'wb') as target: |
1287 | os.fchown(target.fileno(), uid, gid) |
1288 | os.fchmod(target.fileno(), perms) |
1289 | target.write(content) |
1290 | @@ -305,11 +305,11 @@ |
1291 | ceph_client_changed function. |
1292 | """ |
1293 | def wrap(f): |
1294 | - def wrapped_f(*args): |
1295 | + def wrapped_f(*args, **kwargs): |
1296 | checksums = {} |
1297 | for path in restart_map: |
1298 | checksums[path] = file_hash(path) |
1299 | - f(*args) |
1300 | + f(*args, **kwargs) |
1301 | restarts = [] |
1302 | for path in restart_map: |
1303 | if checksums[path] != file_hash(path): |
1304 | @@ -339,12 +339,16 @@ |
1305 | def pwgen(length=None): |
1306 | """Generate a random pasword.""" |
1307 | if length is None: |
1308 | + # A random length is ok to use a weak PRNG |
1309 | length = random.choice(range(35, 45)) |
1310 | alphanumeric_chars = [ |
1311 | l for l in (string.ascii_letters + string.digits) |
1312 | if l not in 'l0QD1vAEIOUaeiou'] |
1313 | + # Use a crypto-friendly PRNG (e.g. /dev/urandom) for making the |
1314 | + # actual password |
1315 | + random_generator = random.SystemRandom() |
1316 | random_chars = [ |
1317 | - random.choice(alphanumeric_chars) for _ in range(length)] |
1318 | + random_generator.choice(alphanumeric_chars) for _ in range(length)] |
1319 | return(''.join(random_chars)) |
1320 | |
1321 | |
1322 | @@ -361,7 +365,7 @@ |
1323 | ip_output = (line for line in ip_output if line) |
1324 | for line in ip_output: |
1325 | if line.split()[1].startswith(int_type): |
1326 | - matched = re.search('.*: (bond[0-9]+\.[0-9]+)@.*', line) |
1327 | + matched = re.search('.*: (' + int_type + r'[0-9]+\.[0-9]+)@.*', line) |
1328 | if matched: |
1329 | interface = matched.groups()[0] |
1330 | else: |
1331 | |
1332 | === modified file 'hooks/charmhelpers/core/services/helpers.py' |
1333 | --- hooks/charmhelpers/core/services/helpers.py 2015-01-29 13:02:55 +0000 |
1334 | +++ hooks/charmhelpers/core/services/helpers.py 2015-04-16 21:56:47 +0000 |
1335 | @@ -45,12 +45,14 @@ |
1336 | """ |
1337 | name = None |
1338 | interface = None |
1339 | - required_keys = [] |
1340 | |
1341 | def __init__(self, name=None, additional_required_keys=None): |
1342 | + if not hasattr(self, 'required_keys'): |
1343 | + self.required_keys = [] |
1344 | + |
1345 | if name is not None: |
1346 | self.name = name |
1347 | - if additional_required_keys is not None: |
1348 | + if additional_required_keys: |
1349 | self.required_keys.extend(additional_required_keys) |
1350 | self.get_data() |
1351 | |
1352 | @@ -134,7 +136,10 @@ |
1353 | """ |
1354 | name = 'db' |
1355 | interface = 'mysql' |
1356 | - required_keys = ['host', 'user', 'password', 'database'] |
1357 | + |
1358 | + def __init__(self, *args, **kwargs): |
1359 | + self.required_keys = ['host', 'user', 'password', 'database'] |
1360 | + RelationContext.__init__(self, *args, **kwargs) |
1361 | |
1362 | |
1363 | class HttpRelation(RelationContext): |
1364 | @@ -146,7 +151,10 @@ |
1365 | """ |
1366 | name = 'website' |
1367 | interface = 'http' |
1368 | - required_keys = ['host', 'port'] |
1369 | + |
1370 | + def __init__(self, *args, **kwargs): |
1371 | + self.required_keys = ['host', 'port'] |
1372 | + RelationContext.__init__(self, *args, **kwargs) |
1373 | |
1374 | def provide_data(self): |
1375 | return { |
1376 | |
1377 | === added file 'hooks/charmhelpers/core/strutils.py' |
1378 | --- hooks/charmhelpers/core/strutils.py 1970-01-01 00:00:00 +0000 |
1379 | +++ hooks/charmhelpers/core/strutils.py 2015-04-16 21:56:47 +0000 |
1380 | @@ -0,0 +1,42 @@ |
1381 | +#!/usr/bin/env python |
1382 | +# -*- coding: utf-8 -*- |
1383 | + |
1384 | +# Copyright 2014-2015 Canonical Limited. |
1385 | +# |
1386 | +# This file is part of charm-helpers. |
1387 | +# |
1388 | +# charm-helpers is free software: you can redistribute it and/or modify |
1389 | +# it under the terms of the GNU Lesser General Public License version 3 as |
1390 | +# published by the Free Software Foundation. |
1391 | +# |
1392 | +# charm-helpers is distributed in the hope that it will be useful, |
1393 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1394 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1395 | +# GNU Lesser General Public License for more details. |
1396 | +# |
1397 | +# You should have received a copy of the GNU Lesser General Public License |
1398 | +# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
1399 | + |
1400 | +import six |
1401 | + |
1402 | + |
1403 | +def bool_from_string(value): |
1404 | + """Interpret string value as boolean. |
1405 | + |
1406 | + Returns True if value translates to True otherwise False. |
1407 | + """ |
1408 | + if isinstance(value, six.string_types): |
1409 | + value = six.text_type(value) |
1410 | + else: |
1411 | + msg = "Unable to interpret non-string value '%s' as boolean" % (value) |
1412 | + raise ValueError(msg) |
1413 | + |
1414 | + value = value.strip().lower() |
1415 | + |
1416 | + if value in ['y', 'yes', 'true', 't', 'on']: |
1417 | + return True |
1418 | + elif value in ['n', 'no', 'false', 'f', 'off']: |
1419 | + return False |
1420 | + |
1421 | + msg = "Unable to interpret string value '%s' as boolean" % (value) |
1422 | + raise ValueError(msg) |
1423 | |
1424 | === modified file 'hooks/charmhelpers/core/sysctl.py' |
1425 | --- hooks/charmhelpers/core/sysctl.py 2015-01-29 13:02:55 +0000 |
1426 | +++ hooks/charmhelpers/core/sysctl.py 2015-04-16 21:56:47 +0000 |
1427 | @@ -17,8 +17,6 @@ |
1428 | # You should have received a copy of the GNU Lesser General Public License |
1429 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
1430 | |
1431 | -__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>' |
1432 | - |
1433 | import yaml |
1434 | |
1435 | from subprocess import check_call |
1436 | @@ -26,25 +24,33 @@ |
1437 | from charmhelpers.core.hookenv import ( |
1438 | log, |
1439 | DEBUG, |
1440 | + ERROR, |
1441 | ) |
1442 | |
1443 | +__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>' |
1444 | + |
1445 | |
1446 | def create(sysctl_dict, sysctl_file): |
1447 | """Creates a sysctl.conf file from a YAML associative array |
1448 | |
1449 | - :param sysctl_dict: a dict of sysctl options eg { 'kernel.max_pid': 1337 } |
1450 | - :type sysctl_dict: dict |
1451 | + :param sysctl_dict: a YAML-formatted string of sysctl options eg "{ 'kernel.max_pid': 1337 }" |
1452 | + :type sysctl_dict: str |
1453 | :param sysctl_file: path to the sysctl file to be saved |
1454 | :type sysctl_file: str or unicode |
1455 | :returns: None |
1456 | """ |
1457 | - sysctl_dict = yaml.load(sysctl_dict) |
1458 | + try: |
1459 | + sysctl_dict_parsed = yaml.safe_load(sysctl_dict) |
1460 | + except yaml.YAMLError: |
1461 | + log("Error parsing YAML sysctl_dict: {}".format(sysctl_dict), |
1462 | + level=ERROR) |
1463 | + return |
1464 | |
1465 | with open(sysctl_file, "w") as fd: |
1466 | - for key, value in sysctl_dict.items(): |
1467 | + for key, value in sysctl_dict_parsed.items(): |
1468 | fd.write("{}={}\n".format(key, value)) |
1469 | |
1470 | - log("Updating sysctl_file: %s values: %s" % (sysctl_file, sysctl_dict), |
1471 | + log("Updating sysctl_file: %s values: %s" % (sysctl_file, sysctl_dict_parsed), |
1472 | level=DEBUG) |
1473 | |
1474 | check_call(["sysctl", "-p", sysctl_file]) |
1475 | |
1476 | === modified file 'hooks/charmhelpers/core/templating.py' |
1477 | --- hooks/charmhelpers/core/templating.py 2015-01-29 13:02:55 +0000 |
1478 | +++ hooks/charmhelpers/core/templating.py 2015-04-16 21:56:47 +0000 |
1479 | @@ -21,7 +21,7 @@ |
1480 | |
1481 | |
1482 | def render(source, target, context, owner='root', group='root', |
1483 | - perms=0o444, templates_dir=None): |
1484 | + perms=0o444, templates_dir=None, encoding='UTF-8'): |
1485 | """ |
1486 | Render a template. |
1487 | |
1488 | @@ -64,5 +64,5 @@ |
1489 | level=hookenv.ERROR) |
1490 | raise e |
1491 | content = template.render(context) |
1492 | - host.mkdir(os.path.dirname(target), owner, group) |
1493 | - host.write_file(target, content, owner, group, perms) |
1494 | + host.mkdir(os.path.dirname(target), owner, group, perms=0o755) |
1495 | + host.write_file(target, content.encode(encoding), owner, group, perms) |
1496 | |
1497 | === added file 'hooks/charmhelpers/core/unitdata.py' |
1498 | --- hooks/charmhelpers/core/unitdata.py 1970-01-01 00:00:00 +0000 |
1499 | +++ hooks/charmhelpers/core/unitdata.py 2015-04-16 21:56:47 +0000 |
1500 | @@ -0,0 +1,477 @@ |
1501 | +#!/usr/bin/env python |
1502 | +# -*- coding: utf-8 -*- |
1503 | +# |
1504 | +# Copyright 2014-2015 Canonical Limited. |
1505 | +# |
1506 | +# This file is part of charm-helpers. |
1507 | +# |
1508 | +# charm-helpers is free software: you can redistribute it and/or modify |
1509 | +# it under the terms of the GNU Lesser General Public License version 3 as |
1510 | +# published by the Free Software Foundation. |
1511 | +# |
1512 | +# charm-helpers is distributed in the hope that it will be useful, |
1513 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1514 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1515 | +# GNU Lesser General Public License for more details. |
1516 | +# |
1517 | +# You should have received a copy of the GNU Lesser General Public License |
1518 | +# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
1519 | +# |
1520 | +# |
1521 | +# Authors: |
1522 | +# Kapil Thangavelu <kapil.foss@gmail.com> |
1523 | +# |
1524 | +""" |
1525 | +Intro |
1526 | +----- |
1527 | + |
1528 | +A simple way to store state in units. This provides a key value |
1529 | +storage with support for versioned, transactional operation, |
1530 | +and can calculate deltas from previous values to simplify unit logic |
1531 | +when processing changes. |
1532 | + |
1533 | + |
1534 | +Hook Integration |
1535 | +---------------- |
1536 | + |
1537 | +There are several extant frameworks for hook execution, including |
1538 | + |
1539 | + - charmhelpers.core.hookenv.Hooks |
1540 | + - charmhelpers.core.services.ServiceManager |
1541 | + |
1542 | +The storage classes are framework agnostic, one simple integration is |
1543 | +via the HookData contextmanager. It will record the current hook |
1544 | +execution environment (including relation data, config data, etc.), |
1545 | +setup a transaction and allow easy access to the changes from |
1546 | +previously seen values. One consequence of the integration is the |
1547 | +reservation of particular keys ('rels', 'unit', 'env', 'config', |
1548 | +'charm_revisions') for their respective values. |
1549 | + |
1550 | +Here's a fully worked integration example using hookenv.Hooks:: |
1551 | + |
1552 | + from charmhelper.core import hookenv, unitdata |
1553 | + |
1554 | + hook_data = unitdata.HookData() |
1555 | + db = unitdata.kv() |
1556 | + hooks = hookenv.Hooks() |
1557 | + |
1558 | + @hooks.hook |
1559 | + def config_changed(): |
1560 | + # Print all changes to configuration from previously seen |
1561 | + # values. |
1562 | + for changed, (prev, cur) in hook_data.conf.items(): |
1563 | + print('config changed', changed, |
1564 | + 'previous value', prev, |
1565 | + 'current value', cur) |
1566 | + |
1567 | + # Get some unit specific bookeeping |
1568 | + if not db.get('pkg_key'): |
1569 | + key = urllib.urlopen('https://example.com/pkg_key').read() |
1570 | + db.set('pkg_key', key) |
1571 | + |
1572 | + # Directly access all charm config as a mapping. |
1573 | + conf = db.getrange('config', True) |
1574 | + |
1575 | + # Directly access all relation data as a mapping |
1576 | + rels = db.getrange('rels', True) |
1577 | + |
1578 | + if __name__ == '__main__': |
1579 | + with hook_data(): |
1580 | + hook.execute() |
1581 | + |
1582 | + |
1583 | +A more basic integration is via the hook_scope context manager which simply |
1584 | +manages transaction scope (and records hook name, and timestamp):: |
1585 | + |
1586 | + >>> from unitdata import kv |
1587 | + >>> db = kv() |
1588 | + >>> with db.hook_scope('install'): |
1589 | + ... # do work, in transactional scope. |
1590 | + ... db.set('x', 1) |
1591 | + >>> db.get('x') |
1592 | + 1 |
1593 | + |
1594 | + |
1595 | +Usage |
1596 | +----- |
1597 | + |
1598 | +Values are automatically json de/serialized to preserve basic typing |
1599 | +and complex data struct capabilities (dicts, lists, ints, booleans, etc). |
1600 | + |
1601 | +Individual values can be manipulated via get/set:: |
1602 | + |
1603 | + >>> kv.set('y', True) |
1604 | + >>> kv.get('y') |
1605 | + True |
1606 | + |
1607 | + # We can set complex values (dicts, lists) as a single key. |
1608 | + >>> kv.set('config', {'a': 1, 'b': True'}) |
1609 | + |
1610 | + # Also supports returning dictionaries as a record which |
1611 | + # provides attribute access. |
1612 | + >>> config = kv.get('config', record=True) |
1613 | + >>> config.b |
1614 | + True |
1615 | + |
1616 | + |
1617 | +Groups of keys can be manipulated with update/getrange:: |
1618 | + |
1619 | + >>> kv.update({'z': 1, 'y': 2}, prefix="gui.") |
1620 | + >>> kv.getrange('gui.', strip=True) |
1621 | + {'z': 1, 'y': 2} |
1622 | + |
1623 | +When updating values, its very helpful to understand which values |
1624 | +have actually changed and how have they changed. The storage |
1625 | +provides a delta method to provide for this:: |
1626 | + |
1627 | + >>> data = {'debug': True, 'option': 2} |
1628 | + >>> delta = kv.delta(data, 'config.') |
1629 | + >>> delta.debug.previous |
1630 | + None |
1631 | + >>> delta.debug.current |
1632 | + True |
1633 | + >>> delta |
1634 | + {'debug': (None, True), 'option': (None, 2)} |
1635 | + |
1636 | +Note the delta method does not persist the actual change, it needs to |
1637 | +be explicitly saved via 'update' method:: |
1638 | + |
1639 | + >>> kv.update(data, 'config.') |
1640 | + |
1641 | +Values modified in the context of a hook scope retain historical values |
1642 | +associated to the hookname. |
1643 | + |
1644 | + >>> with db.hook_scope('config-changed'): |
1645 | + ... db.set('x', 42) |
1646 | + >>> db.gethistory('x') |
1647 | + [(1, u'x', 1, u'install', u'2015-01-21T16:49:30.038372'), |
1648 | + (2, u'x', 42, u'config-changed', u'2015-01-21T16:49:30.038786')] |
1649 | + |
1650 | +""" |
1651 | + |
1652 | +import collections |
1653 | +import contextlib |
1654 | +import datetime |
1655 | +import json |
1656 | +import os |
1657 | +import pprint |
1658 | +import sqlite3 |
1659 | +import sys |
1660 | + |
1661 | +__author__ = 'Kapil Thangavelu <kapil.foss@gmail.com>' |
1662 | + |
1663 | + |
1664 | +class Storage(object): |
1665 | + """Simple key value database for local unit state within charms. |
1666 | + |
1667 | + Modifications are automatically committed at hook exit. That's |
1668 | + currently regardless of exit code. |
1669 | + |
1670 | + To support dicts, lists, integer, floats, and booleans values |
1671 | + are automatically json encoded/decoded. |
1672 | + """ |
1673 | + def __init__(self, path=None): |
1674 | + self.db_path = path |
1675 | + if path is None: |
1676 | + self.db_path = os.path.join( |
1677 | + os.environ.get('CHARM_DIR', ''), '.unit-state.db') |
1678 | + self.conn = sqlite3.connect('%s' % self.db_path) |
1679 | + self.cursor = self.conn.cursor() |
1680 | + self.revision = None |
1681 | + self._closed = False |
1682 | + self._init() |
1683 | + |
1684 | + def close(self): |
1685 | + if self._closed: |
1686 | + return |
1687 | + self.flush(False) |
1688 | + self.cursor.close() |
1689 | + self.conn.close() |
1690 | + self._closed = True |
1691 | + |
1692 | + def _scoped_query(self, stmt, params=None): |
1693 | + if params is None: |
1694 | + params = [] |
1695 | + return stmt, params |
1696 | + |
1697 | + def get(self, key, default=None, record=False): |
1698 | + self.cursor.execute( |
1699 | + *self._scoped_query( |
1700 | + 'select data from kv where key=?', [key])) |
1701 | + result = self.cursor.fetchone() |
1702 | + if not result: |
1703 | + return default |
1704 | + if record: |
1705 | + return Record(json.loads(result[0])) |
1706 | + return json.loads(result[0]) |
1707 | + |
1708 | + def getrange(self, key_prefix, strip=False): |
1709 | + stmt = "select key, data from kv where key like '%s%%'" % key_prefix |
1710 | + self.cursor.execute(*self._scoped_query(stmt)) |
1711 | + result = self.cursor.fetchall() |
1712 | + |
1713 | + if not result: |
1714 | + return None |
1715 | + if not strip: |
1716 | + key_prefix = '' |
1717 | + return dict([ |
1718 | + (k[len(key_prefix):], json.loads(v)) for k, v in result]) |
1719 | + |
1720 | + def update(self, mapping, prefix=""): |
1721 | + for k, v in mapping.items(): |
1722 | + self.set("%s%s" % (prefix, k), v) |
1723 | + |
1724 | + def unset(self, key): |
1725 | + self.cursor.execute('delete from kv where key=?', [key]) |
1726 | + if self.revision and self.cursor.rowcount: |
1727 | + self.cursor.execute( |
1728 | + 'insert into kv_revisions values (?, ?, ?)', |
1729 | + [key, self.revision, json.dumps('DELETED')]) |
1730 | + |
1731 | + def set(self, key, value): |
1732 | + serialized = json.dumps(value) |
1733 | + |
1734 | + self.cursor.execute( |
1735 | + 'select data from kv where key=?', [key]) |
1736 | + exists = self.cursor.fetchone() |
1737 | + |
1738 | + # Skip mutations to the same value |
1739 | + if exists: |
1740 | + if exists[0] == serialized: |
1741 | + return value |
1742 | + |
1743 | + if not exists: |
1744 | + self.cursor.execute( |
1745 | + 'insert into kv (key, data) values (?, ?)', |
1746 | + (key, serialized)) |
1747 | + else: |
1748 | + self.cursor.execute(''' |
1749 | + update kv |
1750 | + set data = ? |
1751 | + where key = ?''', [serialized, key]) |
1752 | + |
1753 | + # Save |
1754 | + if not self.revision: |
1755 | + return value |
1756 | + |
1757 | + self.cursor.execute( |
1758 | + 'select 1 from kv_revisions where key=? and revision=?', |
1759 | + [key, self.revision]) |
1760 | + exists = self.cursor.fetchone() |
1761 | + |
1762 | + if not exists: |
1763 | + self.cursor.execute( |
1764 | + '''insert into kv_revisions ( |
1765 | + revision, key, data) values (?, ?, ?)''', |
1766 | + (self.revision, key, serialized)) |
1767 | + else: |
1768 | + self.cursor.execute( |
1769 | + ''' |
1770 | + update kv_revisions |
1771 | + set data = ? |
1772 | + where key = ? |
1773 | + and revision = ?''', |
1774 | + [serialized, key, self.revision]) |
1775 | + |
1776 | + return value |
1777 | + |
1778 | + def delta(self, mapping, prefix): |
1779 | + """ |
1780 | + return a delta containing values that have changed. |
1781 | + """ |
1782 | + previous = self.getrange(prefix, strip=True) |
1783 | + if not previous: |
1784 | + pk = set() |
1785 | + else: |
1786 | + pk = set(previous.keys()) |
1787 | + ck = set(mapping.keys()) |
1788 | + delta = DeltaSet() |
1789 | + |
1790 | + # added |
1791 | + for k in ck.difference(pk): |
1792 | + delta[k] = Delta(None, mapping[k]) |
1793 | + |
1794 | + # removed |
1795 | + for k in pk.difference(ck): |
1796 | + delta[k] = Delta(previous[k], None) |
1797 | + |
1798 | + # changed |
1799 | + for k in pk.intersection(ck): |
1800 | + c = mapping[k] |
1801 | + p = previous[k] |
1802 | + if c != p: |
1803 | + delta[k] = Delta(p, c) |
1804 | + |
1805 | + return delta |
1806 | + |
1807 | + @contextlib.contextmanager |
1808 | + def hook_scope(self, name=""): |
1809 | + """Scope all future interactions to the current hook execution |
1810 | + revision.""" |
1811 | + assert not self.revision |
1812 | + self.cursor.execute( |
1813 | + 'insert into hooks (hook, date) values (?, ?)', |
1814 | + (name or sys.argv[0], |
1815 | + datetime.datetime.utcnow().isoformat())) |
1816 | + self.revision = self.cursor.lastrowid |
1817 | + try: |
1818 | + yield self.revision |
1819 | + self.revision = None |
1820 | + except: |
1821 | + self.flush(False) |
1822 | + self.revision = None |
1823 | + raise |
1824 | + else: |
1825 | + self.flush() |
1826 | + |
1827 | + def flush(self, save=True): |
1828 | + if save: |
1829 | + self.conn.commit() |
1830 | + elif self._closed: |
1831 | + return |
1832 | + else: |
1833 | + self.conn.rollback() |
1834 | + |
1835 | + def _init(self): |
1836 | + self.cursor.execute(''' |
1837 | + create table if not exists kv ( |
1838 | + key text, |
1839 | + data text, |
1840 | + primary key (key) |
1841 | + )''') |
1842 | + self.cursor.execute(''' |
1843 | + create table if not exists kv_revisions ( |
1844 | + key text, |
1845 | + revision integer, |
1846 | + data text, |
1847 | + primary key (key, revision) |
1848 | + )''') |
1849 | + self.cursor.execute(''' |
1850 | + create table if not exists hooks ( |
1851 | + version integer primary key autoincrement, |
1852 | + hook text, |
1853 | + date text |
1854 | + )''') |
1855 | + self.conn.commit() |
1856 | + |
1857 | + def gethistory(self, key, deserialize=False): |
1858 | + self.cursor.execute( |
1859 | + ''' |
1860 | + select kv.revision, kv.key, kv.data, h.hook, h.date |
1861 | + from kv_revisions kv, |
1862 | + hooks h |
1863 | + where kv.key=? |
1864 | + and kv.revision = h.version |
1865 | + ''', [key]) |
1866 | + if deserialize is False: |
1867 | + return self.cursor.fetchall() |
1868 | + return map(_parse_history, self.cursor.fetchall()) |
1869 | + |
1870 | + def debug(self, fh=sys.stderr): |
1871 | + self.cursor.execute('select * from kv') |
1872 | + pprint.pprint(self.cursor.fetchall(), stream=fh) |
1873 | + self.cursor.execute('select * from kv_revisions') |
1874 | + pprint.pprint(self.cursor.fetchall(), stream=fh) |
1875 | + |
1876 | + |
1877 | +def _parse_history(d): |
1878 | + return (d[0], d[1], json.loads(d[2]), d[3], |
1879 | + datetime.datetime.strptime(d[-1], "%Y-%m-%dT%H:%M:%S.%f")) |
1880 | + |
1881 | + |
1882 | +class HookData(object): |
1883 | + """Simple integration for existing hook exec frameworks. |
1884 | + |
1885 | + Records all unit information, and stores deltas for processing |
1886 | + by the hook. |
1887 | + |
1888 | + Sample:: |
1889 | + |
1890 | + from charmhelper.core import hookenv, unitdata |
1891 | + |
1892 | + changes = unitdata.HookData() |
1893 | + db = unitdata.kv() |
1894 | + hooks = hookenv.Hooks() |
1895 | + |
1896 | + @hooks.hook |
1897 | + def config_changed(): |
1898 | + # View all changes to configuration |
1899 | + for changed, (prev, cur) in changes.conf.items(): |
1900 | + print('config changed', changed, |
1901 | + 'previous value', prev, |
1902 | + 'current value', cur) |
1903 | + |
1904 | + # Get some unit specific bookeeping |
1905 | + if not db.get('pkg_key'): |
1906 | + key = urllib.urlopen('https://example.com/pkg_key').read() |
1907 | + db.set('pkg_key', key) |
1908 | + |
1909 | + if __name__ == '__main__': |
1910 | + with changes(): |
1911 | + hook.execute() |
1912 | + |
1913 | + """ |
1914 | + def __init__(self): |
1915 | + self.kv = kv() |
1916 | + self.conf = None |
1917 | + self.rels = None |
1918 | + |
1919 | + @contextlib.contextmanager |
1920 | + def __call__(self): |
1921 | + from charmhelpers.core import hookenv |
1922 | + hook_name = hookenv.hook_name() |
1923 | + |
1924 | + with self.kv.hook_scope(hook_name): |
1925 | + self._record_charm_version(hookenv.charm_dir()) |
1926 | + delta_config, delta_relation = self._record_hook(hookenv) |
1927 | + yield self.kv, delta_config, delta_relation |
1928 | + |
1929 | + def _record_charm_version(self, charm_dir): |
1930 | + # Record revisions.. charm revisions are meaningless |
1931 | + # to charm authors as they don't control the revision. |
1932 | + # so logic dependnent on revision is not particularly |
1933 | + # useful, however it is useful for debugging analysis. |
1934 | + charm_rev = open( |
1935 | + os.path.join(charm_dir, 'revision')).read().strip() |
1936 | + charm_rev = charm_rev or '0' |
1937 | + revs = self.kv.get('charm_revisions', []) |
1938 | + if charm_rev not in revs: |
1939 | + revs.append(charm_rev.strip() or '0') |
1940 | + self.kv.set('charm_revisions', revs) |
1941 | + |
1942 | + def _record_hook(self, hookenv): |
1943 | + data = hookenv.execution_environment() |
1944 | + self.conf = conf_delta = self.kv.delta(data['conf'], 'config') |
1945 | + self.rels = rels_delta = self.kv.delta(data['rels'], 'rels') |
1946 | + self.kv.set('env', dict(data['env'])) |
1947 | + self.kv.set('unit', data['unit']) |
1948 | + self.kv.set('relid', data.get('relid')) |
1949 | + return conf_delta, rels_delta |
1950 | + |
1951 | + |
1952 | +class Record(dict): |
1953 | + |
1954 | + __slots__ = () |
1955 | + |
1956 | + def __getattr__(self, k): |
1957 | + if k in self: |
1958 | + return self[k] |
1959 | + raise AttributeError(k) |
1960 | + |
1961 | + |
1962 | +class DeltaSet(Record): |
1963 | + |
1964 | + __slots__ = () |
1965 | + |
1966 | + |
1967 | +Delta = collections.namedtuple('Delta', ['previous', 'current']) |
1968 | + |
1969 | + |
1970 | +_KV = None |
1971 | + |
1972 | + |
1973 | +def kv(): |
1974 | + global _KV |
1975 | + if _KV is None: |
1976 | + _KV = Storage() |
1977 | + return _KV |
1978 | |
1979 | === modified file 'hooks/charmhelpers/fetch/archiveurl.py' |
1980 | --- hooks/charmhelpers/fetch/archiveurl.py 2015-01-29 13:02:55 +0000 |
1981 | +++ hooks/charmhelpers/fetch/archiveurl.py 2015-04-16 21:56:47 +0000 |
1982 | @@ -18,6 +18,16 @@ |
1983 | import hashlib |
1984 | import re |
1985 | |
1986 | +from charmhelpers.fetch import ( |
1987 | + BaseFetchHandler, |
1988 | + UnhandledSource |
1989 | +) |
1990 | +from charmhelpers.payload.archive import ( |
1991 | + get_archive_handler, |
1992 | + extract, |
1993 | +) |
1994 | +from charmhelpers.core.host import mkdir, check_hash |
1995 | + |
1996 | import six |
1997 | if six.PY3: |
1998 | from urllib.request import ( |
1999 | @@ -35,16 +45,6 @@ |
2000 | ) |
2001 | from urlparse import urlparse, urlunparse, parse_qs |
2002 | |
2003 | -from charmhelpers.fetch import ( |
2004 | - BaseFetchHandler, |
2005 | - UnhandledSource |
2006 | -) |
2007 | -from charmhelpers.payload.archive import ( |
2008 | - get_archive_handler, |
2009 | - extract, |
2010 | -) |
2011 | -from charmhelpers.core.host import mkdir, check_hash |
2012 | - |
2013 | |
2014 | def splituser(host): |
2015 | '''urllib.splituser(), but six's support of this seems broken''' |
2016 | |
2017 | === modified file 'hooks/charmhelpers/fetch/giturl.py' |
2018 | --- hooks/charmhelpers/fetch/giturl.py 2015-01-29 13:02:55 +0000 |
2019 | +++ hooks/charmhelpers/fetch/giturl.py 2015-04-16 21:56:47 +0000 |
2020 | @@ -32,7 +32,7 @@ |
2021 | apt_install("python-git") |
2022 | from git import Repo |
2023 | |
2024 | -from git.exc import GitCommandError |
2025 | +from git.exc import GitCommandError # noqa E402 |
2026 | |
2027 | |
2028 | class GitUrlFetchHandler(BaseFetchHandler): |
2029 | |
2030 | === modified file 'tests/charmhelpers/contrib/amulet/utils.py' |
2031 | --- tests/charmhelpers/contrib/amulet/utils.py 2015-01-29 13:02:55 +0000 |
2032 | +++ tests/charmhelpers/contrib/amulet/utils.py 2015-04-16 21:56:47 +0000 |
2033 | @@ -118,6 +118,9 @@ |
2034 | longs, or can be a function that evaluate a variable and returns a |
2035 | bool. |
2036 | """ |
2037 | + self.log.debug('actual: {}'.format(repr(actual))) |
2038 | + self.log.debug('expected: {}'.format(repr(expected))) |
2039 | + |
2040 | for k, v in six.iteritems(expected): |
2041 | if k in actual: |
2042 | if (isinstance(v, six.string_types) or |
2043 | @@ -134,7 +137,6 @@ |
2044 | def validate_relation_data(self, sentry_unit, relation, expected): |
2045 | """Validate actual relation data based on expected relation data.""" |
2046 | actual = sentry_unit.relation(relation[0], relation[1]) |
2047 | - self.log.debug('actual: {}'.format(repr(actual))) |
2048 | return self._validate_dict_data(expected, actual) |
2049 | |
2050 | def _validate_list_data(self, expected, actual): |
2051 | @@ -169,8 +171,13 @@ |
2052 | cmd = 'pgrep -o -f {}'.format(service) |
2053 | else: |
2054 | cmd = 'pgrep -o {}'.format(service) |
2055 | - proc_dir = '/proc/{}'.format(sentry_unit.run(cmd)[0].strip()) |
2056 | - return self._get_dir_mtime(sentry_unit, proc_dir) |
2057 | + cmd = cmd + ' | grep -v pgrep || exit 0' |
2058 | + cmd_out = sentry_unit.run(cmd) |
2059 | + self.log.debug('CMDout: ' + str(cmd_out)) |
2060 | + if cmd_out[0]: |
2061 | + self.log.debug('Pid for %s %s' % (service, str(cmd_out[0]))) |
2062 | + proc_dir = '/proc/{}'.format(cmd_out[0].strip()) |
2063 | + return self._get_dir_mtime(sentry_unit, proc_dir) |
2064 | |
2065 | def service_restarted(self, sentry_unit, service, filename, |
2066 | pgrep_full=False, sleep_time=20): |
2067 | @@ -187,6 +194,121 @@ |
2068 | else: |
2069 | return False |
2070 | |
2071 | + def service_restarted_since(self, sentry_unit, mtime, service, |
2072 | + pgrep_full=False, sleep_time=20, |
2073 | + retry_count=2): |
2074 | + """Check if service was been started after a given time. |
2075 | + |
2076 | + Args: |
2077 | + sentry_unit (sentry): The sentry unit to check for the service on |
2078 | + mtime (float): The epoch time to check against |
2079 | + service (string): service name to look for in process table |
2080 | + pgrep_full (boolean): Use full command line search mode with pgrep |
2081 | + sleep_time (int): Seconds to sleep before looking for process |
2082 | + retry_count (int): If service is not found, how many times to retry |
2083 | + |
2084 | + Returns: |
2085 | + bool: True if service found and its start time it newer than mtime, |
2086 | + False if service is older than mtime or if service was |
2087 | + not found. |
2088 | + """ |
2089 | + self.log.debug('Checking %s restarted since %s' % (service, mtime)) |
2090 | + time.sleep(sleep_time) |
2091 | + proc_start_time = self._get_proc_start_time(sentry_unit, service, |
2092 | + pgrep_full) |
2093 | + while retry_count > 0 and not proc_start_time: |
2094 | + self.log.debug('No pid file found for service %s, will retry %i ' |
2095 | + 'more times' % (service, retry_count)) |
2096 | + time.sleep(30) |
2097 | + proc_start_time = self._get_proc_start_time(sentry_unit, service, |
2098 | + pgrep_full) |
2099 | + retry_count = retry_count - 1 |
2100 | + |
2101 | + if not proc_start_time: |
2102 | + self.log.warn('No proc start time found, assuming service did ' |
2103 | + 'not start') |
2104 | + return False |
2105 | + if proc_start_time >= mtime: |
2106 | + self.log.debug('proc start time is newer than provided mtime' |
2107 | + '(%s >= %s)' % (proc_start_time, mtime)) |
2108 | + return True |
2109 | + else: |
2110 | + self.log.warn('proc start time (%s) is older than provided mtime ' |
2111 | + '(%s), service did not restart' % (proc_start_time, |
2112 | + mtime)) |
2113 | + return False |
2114 | + |
2115 | + def config_updated_since(self, sentry_unit, filename, mtime, |
2116 | + sleep_time=20): |
2117 | + """Check if file was modified after a given time. |
2118 | + |
2119 | + Args: |
2120 | + sentry_unit (sentry): The sentry unit to check the file mtime on |
2121 | + filename (string): The file to check mtime of |
2122 | + mtime (float): The epoch time to check against |
2123 | + sleep_time (int): Seconds to sleep before looking for process |
2124 | + |
2125 | + Returns: |
2126 | + bool: True if file was modified more recently than mtime, False if |
2127 | + file was modified before mtime, |
2128 | + """ |
2129 | + self.log.debug('Checking %s updated since %s' % (filename, mtime)) |
2130 | + time.sleep(sleep_time) |
2131 | + file_mtime = self._get_file_mtime(sentry_unit, filename) |
2132 | + if file_mtime >= mtime: |
2133 | + self.log.debug('File mtime is newer than provided mtime ' |
2134 | + '(%s >= %s)' % (file_mtime, mtime)) |
2135 | + return True |
2136 | + else: |
2137 | + self.log.warn('File mtime %s is older than provided mtime %s' |
2138 | + % (file_mtime, mtime)) |
2139 | + return False |
2140 | + |
2141 | + def validate_service_config_changed(self, sentry_unit, mtime, service, |
2142 | + filename, pgrep_full=False, |
2143 | + sleep_time=20, retry_count=2): |
2144 | + """Check service and file were updated after mtime |
2145 | + |
2146 | + Args: |
2147 | + sentry_unit (sentry): The sentry unit to check for the service on |
2148 | + mtime (float): The epoch time to check against |
2149 | + service (string): service name to look for in process table |
2150 | + filename (string): The file to check mtime of |
2151 | + pgrep_full (boolean): Use full command line search mode with pgrep |
2152 | + sleep_time (int): Seconds to sleep before looking for process |
2153 | + retry_count (int): If service is not found, how many times to retry |
2154 | + |
2155 | + Typical Usage: |
2156 | + u = OpenStackAmuletUtils(ERROR) |
2157 | + ... |
2158 | + mtime = u.get_sentry_time(self.cinder_sentry) |
2159 | + self.d.configure('cinder', {'verbose': 'True', 'debug': 'True'}) |
2160 | + if not u.validate_service_config_changed(self.cinder_sentry, |
2161 | + mtime, |
2162 | + 'cinder-api', |
2163 | + '/etc/cinder/cinder.conf') |
2164 | + amulet.raise_status(amulet.FAIL, msg='update failed') |
2165 | + Returns: |
2166 | + bool: True if both service and file where updated/restarted after |
2167 | + mtime, False if service is older than mtime or if service was |
2168 | + not found or if filename was modified before mtime. |
2169 | + """ |
2170 | + self.log.debug('Checking %s restarted since %s' % (service, mtime)) |
2171 | + time.sleep(sleep_time) |
2172 | + service_restart = self.service_restarted_since(sentry_unit, mtime, |
2173 | + service, |
2174 | + pgrep_full=pgrep_full, |
2175 | + sleep_time=0, |
2176 | + retry_count=retry_count) |
2177 | + config_update = self.config_updated_since(sentry_unit, filename, mtime, |
2178 | + sleep_time=0) |
2179 | + return service_restart and config_update |
2180 | + |
2181 | + def get_sentry_time(self, sentry_unit): |
2182 | + """Return current epoch time on a sentry""" |
2183 | + cmd = "date +'%s'" |
2184 | + return float(sentry_unit.run(cmd)[0]) |
2185 | + |
2186 | def relation_error(self, name, data): |
2187 | return 'unexpected relation data in {} - {}'.format(name, data) |
2188 | |
2189 | |
2190 | === modified file 'tests/charmhelpers/contrib/openstack/amulet/deployment.py' |
2191 | --- tests/charmhelpers/contrib/openstack/amulet/deployment.py 2015-01-29 13:02:55 +0000 |
2192 | +++ tests/charmhelpers/contrib/openstack/amulet/deployment.py 2015-04-16 21:56:47 +0000 |
2193 | @@ -15,6 +15,7 @@ |
2194 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
2195 | |
2196 | import six |
2197 | +from collections import OrderedDict |
2198 | from charmhelpers.contrib.amulet.deployment import ( |
2199 | AmuletDeployment |
2200 | ) |
2201 | @@ -43,7 +44,7 @@ |
2202 | Determine if the local branch being tested is derived from its |
2203 | stable or next (dev) branch, and based on this, use the corresonding |
2204 | stable or next branches for the other_services.""" |
2205 | - base_charms = ['mysql', 'mongodb', 'rabbitmq-server'] |
2206 | + base_charms = ['mysql', 'mongodb'] |
2207 | |
2208 | if self.stable: |
2209 | for svc in other_services: |
2210 | @@ -71,16 +72,19 @@ |
2211 | services.append(this_service) |
2212 | use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph', |
2213 | 'ceph-osd', 'ceph-radosgw'] |
2214 | + # Openstack subordinate charms do not expose an origin option as that |
2215 | + # is controlled by the principle |
2216 | + ignore = ['neutron-openvswitch'] |
2217 | |
2218 | if self.openstack: |
2219 | for svc in services: |
2220 | - if svc['name'] not in use_source: |
2221 | + if svc['name'] not in use_source + ignore: |
2222 | config = {'openstack-origin': self.openstack} |
2223 | self.d.configure(svc['name'], config) |
2224 | |
2225 | if self.source: |
2226 | for svc in services: |
2227 | - if svc['name'] in use_source: |
2228 | + if svc['name'] in use_source and svc['name'] not in ignore: |
2229 | config = {'source': self.source} |
2230 | self.d.configure(svc['name'], config) |
2231 | |
2232 | @@ -97,12 +101,37 @@ |
2233 | """ |
2234 | (self.precise_essex, self.precise_folsom, self.precise_grizzly, |
2235 | self.precise_havana, self.precise_icehouse, |
2236 | - self.trusty_icehouse) = range(6) |
2237 | + self.trusty_icehouse, self.trusty_juno, self.trusty_kilo, |
2238 | + self.utopic_juno, self.vivid_kilo) = range(10) |
2239 | releases = { |
2240 | ('precise', None): self.precise_essex, |
2241 | ('precise', 'cloud:precise-folsom'): self.precise_folsom, |
2242 | ('precise', 'cloud:precise-grizzly'): self.precise_grizzly, |
2243 | ('precise', 'cloud:precise-havana'): self.precise_havana, |
2244 | ('precise', 'cloud:precise-icehouse'): self.precise_icehouse, |
2245 | - ('trusty', None): self.trusty_icehouse} |
2246 | + ('trusty', None): self.trusty_icehouse, |
2247 | + ('trusty', 'cloud:trusty-juno'): self.trusty_juno, |
2248 | + ('trusty', 'cloud:trusty-kilo'): self.trusty_kilo, |
2249 | + ('utopic', None): self.utopic_juno, |
2250 | + ('vivid', None): self.vivid_kilo} |
2251 | return releases[(self.series, self.openstack)] |
2252 | + |
2253 | + def _get_openstack_release_string(self): |
2254 | + """Get openstack release string. |
2255 | + |
2256 | + Return a string representing the openstack release. |
2257 | + """ |
2258 | + releases = OrderedDict([ |
2259 | + ('precise', 'essex'), |
2260 | + ('quantal', 'folsom'), |
2261 | + ('raring', 'grizzly'), |
2262 | + ('saucy', 'havana'), |
2263 | + ('trusty', 'icehouse'), |
2264 | + ('utopic', 'juno'), |
2265 | + ('vivid', 'kilo'), |
2266 | + ]) |
2267 | + if self.openstack: |
2268 | + os_origin = self.openstack.split(':')[1] |
2269 | + return os_origin.split('%s-' % self.series)[1].split('/')[0] |
2270 | + else: |
2271 | + return releases[self.series] |
charm_lint_check #3526 nova-cell-next for 1chb1n mp256591
LINT OK: passed
Build: http:// 10.245. 162.77: 8080/job/ charm_lint_ check/3526/