Merge lp:~corey.bryant/charms/trusty/openstack-dashboard/fix-global-reqs into lp:~openstack-charmers-archive/charms/trusty/openstack-dashboard/next

Proposed by Corey Bryant
Status: Merged
Approved by: Billy Olsen
Approved revision: 68
Merged at revision: 67
Proposed branch: lp:~corey.bryant/charms/trusty/openstack-dashboard/fix-global-reqs
Merge into: lp:~openstack-charmers-archive/charms/trusty/openstack-dashboard/next
Diff against target: 1554 lines (+687/-111)
15 files modified
hooks/charmhelpers/contrib/hahelpers/cluster.py (+47/-3)
hooks/charmhelpers/contrib/openstack/amulet/deployment.py (+6/-2)
hooks/charmhelpers/contrib/openstack/amulet/utils.py (+122/-3)
hooks/charmhelpers/contrib/openstack/context.py (+1/-1)
hooks/charmhelpers/contrib/openstack/ip.py (+49/-44)
hooks/charmhelpers/contrib/openstack/neutron.py (+16/-9)
hooks/charmhelpers/contrib/openstack/utils.py (+21/-8)
hooks/charmhelpers/core/hookenv.py (+147/-10)
hooks/charmhelpers/core/host.py (+25/-7)
hooks/charmhelpers/core/services/base.py (+32/-11)
hooks/charmhelpers/fetch/__init__.py (+1/-1)
tests/charmhelpers/contrib/amulet/utils.py (+91/-6)
tests/charmhelpers/contrib/openstack/amulet/deployment.py (+6/-2)
tests/charmhelpers/contrib/openstack/amulet/utils.py (+122/-3)
unit_tests/test_horizon_hooks.py (+1/-1)
To merge this branch: bzr merge lp:~corey.bryant/charms/trusty/openstack-dashboard/fix-global-reqs
Reviewer Review Type Date Requested Status
Billy Olsen Approve
Review via email: mp+262472@code.launchpad.net

Description of the change

To post a comment you must log in.
Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_lint_check #5475 openstack-dashboard-next for corey.bryant mp262472
    LINT OK: passed

Build: http://10.245.162.77:8080/job/charm_lint_check/5475/

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_unit_test #5107 openstack-dashboard-next for corey.bryant mp262472
    UNIT OK: passed

Build: http://10.245.162.77:8080/job/charm_unit_test/5107/

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_amulet_test #4682 openstack-dashboard-next for corey.bryant mp262472
    AMULET OK: passed

Build: http://10.245.162.77:8080/job/charm_amulet_test/4682/

Revision history for this message
Billy Olsen (billy-olsen) wrote :

LGTM, Approved

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'hooks/charmhelpers/contrib/hahelpers/cluster.py'
2--- hooks/charmhelpers/contrib/hahelpers/cluster.py 2015-02-26 10:11:26 +0000
3+++ hooks/charmhelpers/contrib/hahelpers/cluster.py 2015-06-19 16:15:40 +0000
4@@ -44,6 +44,7 @@
5 ERROR,
6 WARNING,
7 unit_get,
8+ is_leader as juju_is_leader
9 )
10 from charmhelpers.core.decorators import (
11 retry_on_exception,
12@@ -52,6 +53,8 @@
13 bool_from_string,
14 )
15
16+DC_RESOURCE_NAME = 'DC'
17+
18
19 class HAIncompleteConfig(Exception):
20 pass
21@@ -61,17 +64,30 @@
22 pass
23
24
25+class CRMDCNotFound(Exception):
26+ pass
27+
28+
29 def is_elected_leader(resource):
30 """
31 Returns True if the charm executing this is the elected cluster leader.
32
33 It relies on two mechanisms to determine leadership:
34- 1. If the charm is part of a corosync cluster, call corosync to
35+ 1. If juju is sufficiently new and leadership election is supported,
36+ the is_leader command will be used.
37+ 2. If the charm is part of a corosync cluster, call corosync to
38 determine leadership.
39- 2. If the charm is not part of a corosync cluster, the leader is
40+ 3. If the charm is not part of a corosync cluster, the leader is
41 determined as being "the alive unit with the lowest unit numer". In
42 other words, the oldest surviving unit.
43 """
44+ try:
45+ return juju_is_leader()
46+ except NotImplementedError:
47+ log('Juju leadership election feature not enabled'
48+ ', using fallback support',
49+ level=WARNING)
50+
51 if is_clustered():
52 if not is_crm_leader(resource):
53 log('Deferring action to CRM leader.', level=INFO)
54@@ -95,7 +111,33 @@
55 return False
56
57
58-@retry_on_exception(5, base_delay=2, exc_type=CRMResourceNotFound)
59+def is_crm_dc():
60+ """
61+ Determine leadership by querying the pacemaker Designated Controller
62+ """
63+ cmd = ['crm', 'status']
64+ try:
65+ status = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
66+ if not isinstance(status, six.text_type):
67+ status = six.text_type(status, "utf-8")
68+ except subprocess.CalledProcessError as ex:
69+ raise CRMDCNotFound(str(ex))
70+
71+ current_dc = ''
72+ for line in status.split('\n'):
73+ if line.startswith('Current DC'):
74+ # Current DC: juju-lytrusty-machine-2 (168108163) - partition with quorum
75+ current_dc = line.split(':')[1].split()[0]
76+ if current_dc == get_unit_hostname():
77+ return True
78+ elif current_dc == 'NONE':
79+ raise CRMDCNotFound('Current DC: NONE')
80+
81+ return False
82+
83+
84+@retry_on_exception(5, base_delay=2,
85+ exc_type=(CRMResourceNotFound, CRMDCNotFound))
86 def is_crm_leader(resource, retry=False):
87 """
88 Returns True if the charm calling this is the elected corosync leader,
89@@ -104,6 +146,8 @@
90 We allow this operation to be retried to avoid the possibility of getting a
91 false negative. See LP #1396246 for more info.
92 """
93+ if resource == DC_RESOURCE_NAME:
94+ return is_crm_dc()
95 cmd = ['crm', 'resource', 'show', resource]
96 try:
97 status = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
98
99=== modified file 'hooks/charmhelpers/contrib/openstack/amulet/deployment.py'
100--- hooks/charmhelpers/contrib/openstack/amulet/deployment.py 2015-05-05 20:28:18 +0000
101+++ hooks/charmhelpers/contrib/openstack/amulet/deployment.py 2015-06-19 16:15:40 +0000
102@@ -110,7 +110,8 @@
103 (self.precise_essex, self.precise_folsom, self.precise_grizzly,
104 self.precise_havana, self.precise_icehouse,
105 self.trusty_icehouse, self.trusty_juno, self.utopic_juno,
106- self.trusty_kilo, self.vivid_kilo) = range(10)
107+ self.trusty_kilo, self.vivid_kilo, self.trusty_liberty,
108+ self.wily_liberty) = range(12)
109
110 releases = {
111 ('precise', None): self.precise_essex,
112@@ -121,8 +122,10 @@
113 ('trusty', None): self.trusty_icehouse,
114 ('trusty', 'cloud:trusty-juno'): self.trusty_juno,
115 ('trusty', 'cloud:trusty-kilo'): self.trusty_kilo,
116+ ('trusty', 'cloud:trusty-liberty'): self.trusty_liberty,
117 ('utopic', None): self.utopic_juno,
118- ('vivid', None): self.vivid_kilo}
119+ ('vivid', None): self.vivid_kilo,
120+ ('wily', None): self.wily_liberty}
121 return releases[(self.series, self.openstack)]
122
123 def _get_openstack_release_string(self):
124@@ -138,6 +141,7 @@
125 ('trusty', 'icehouse'),
126 ('utopic', 'juno'),
127 ('vivid', 'kilo'),
128+ ('wily', 'liberty'),
129 ])
130 if self.openstack:
131 os_origin = self.openstack.split(':')[1]
132
133=== modified file 'hooks/charmhelpers/contrib/openstack/amulet/utils.py'
134--- hooks/charmhelpers/contrib/openstack/amulet/utils.py 2015-01-26 09:46:38 +0000
135+++ hooks/charmhelpers/contrib/openstack/amulet/utils.py 2015-06-19 16:15:40 +0000
136@@ -16,15 +16,15 @@
137
138 import logging
139 import os
140+import six
141 import time
142 import urllib
143
144 import glanceclient.v1.client as glance_client
145+import heatclient.v1.client as heat_client
146 import keystoneclient.v2_0 as keystone_client
147 import novaclient.v1_1.client as nova_client
148
149-import six
150-
151 from charmhelpers.contrib.amulet.utils import (
152 AmuletUtils
153 )
154@@ -37,7 +37,7 @@
155 """OpenStack amulet utilities.
156
157 This class inherits from AmuletUtils and has additional support
158- that is specifically for use by OpenStack charms.
159+ that is specifically for use by OpenStack charm tests.
160 """
161
162 def __init__(self, log_level=ERROR):
163@@ -51,6 +51,8 @@
164 Validate actual endpoint data vs expected endpoint data. The ports
165 are used to find the matching endpoint.
166 """
167+ self.log.debug('Validating endpoint data...')
168+ self.log.debug('actual: {}'.format(repr(endpoints)))
169 found = False
170 for ep in endpoints:
171 self.log.debug('endpoint: {}'.format(repr(ep)))
172@@ -77,6 +79,7 @@
173 Validate a list of actual service catalog endpoints vs a list of
174 expected service catalog endpoints.
175 """
176+ self.log.debug('Validating service catalog endpoint data...')
177 self.log.debug('actual: {}'.format(repr(actual)))
178 for k, v in six.iteritems(expected):
179 if k in actual:
180@@ -93,6 +96,7 @@
181 Validate a list of actual tenant data vs list of expected tenant
182 data.
183 """
184+ self.log.debug('Validating tenant data...')
185 self.log.debug('actual: {}'.format(repr(actual)))
186 for e in expected:
187 found = False
188@@ -114,6 +118,7 @@
189 Validate a list of actual role data vs a list of expected role
190 data.
191 """
192+ self.log.debug('Validating role data...')
193 self.log.debug('actual: {}'.format(repr(actual)))
194 for e in expected:
195 found = False
196@@ -134,6 +139,7 @@
197 Validate a list of actual user data vs a list of expected user
198 data.
199 """
200+ self.log.debug('Validating user data...')
201 self.log.debug('actual: {}'.format(repr(actual)))
202 for e in expected:
203 found = False
204@@ -155,17 +161,20 @@
205
206 Validate a list of actual flavors vs a list of expected flavors.
207 """
208+ self.log.debug('Validating flavor data...')
209 self.log.debug('actual: {}'.format(repr(actual)))
210 act = [a.name for a in actual]
211 return self._validate_list_data(expected, act)
212
213 def tenant_exists(self, keystone, tenant):
214 """Return True if tenant exists."""
215+ self.log.debug('Checking if tenant exists ({})...'.format(tenant))
216 return tenant in [t.name for t in keystone.tenants.list()]
217
218 def authenticate_keystone_admin(self, keystone_sentry, user, password,
219 tenant):
220 """Authenticates admin user with the keystone admin endpoint."""
221+ self.log.debug('Authenticating keystone admin...')
222 unit = keystone_sentry
223 service_ip = unit.relation('shared-db',
224 'mysql:shared-db')['private-address']
225@@ -175,6 +184,7 @@
226
227 def authenticate_keystone_user(self, keystone, user, password, tenant):
228 """Authenticates a regular user with the keystone public endpoint."""
229+ self.log.debug('Authenticating keystone user ({})...'.format(user))
230 ep = keystone.service_catalog.url_for(service_type='identity',
231 endpoint_type='publicURL')
232 return keystone_client.Client(username=user, password=password,
233@@ -182,12 +192,21 @@
234
235 def authenticate_glance_admin(self, keystone):
236 """Authenticates admin user with glance."""
237+ self.log.debug('Authenticating glance admin...')
238 ep = keystone.service_catalog.url_for(service_type='image',
239 endpoint_type='adminURL')
240 return glance_client.Client(ep, token=keystone.auth_token)
241
242+ def authenticate_heat_admin(self, keystone):
243+ """Authenticates the admin user with heat."""
244+ self.log.debug('Authenticating heat admin...')
245+ ep = keystone.service_catalog.url_for(service_type='orchestration',
246+ endpoint_type='publicURL')
247+ return heat_client.Client(endpoint=ep, token=keystone.auth_token)
248+
249 def authenticate_nova_user(self, keystone, user, password, tenant):
250 """Authenticates a regular user with nova-api."""
251+ self.log.debug('Authenticating nova user ({})...'.format(user))
252 ep = keystone.service_catalog.url_for(service_type='identity',
253 endpoint_type='publicURL')
254 return nova_client.Client(username=user, api_key=password,
255@@ -195,6 +214,7 @@
256
257 def create_cirros_image(self, glance, image_name):
258 """Download the latest cirros image and upload it to glance."""
259+ self.log.debug('Creating glance image ({})...'.format(image_name))
260 http_proxy = os.getenv('AMULET_HTTP_PROXY')
261 self.log.debug('AMULET_HTTP_PROXY: {}'.format(http_proxy))
262 if http_proxy:
263@@ -235,6 +255,11 @@
264
265 def delete_image(self, glance, image):
266 """Delete the specified image."""
267+
268+ # /!\ DEPRECATION WARNING
269+ self.log.warn('/!\\ DEPRECATION WARNING: use '
270+ 'delete_resource instead of delete_image.')
271+ self.log.debug('Deleting glance image ({})...'.format(image))
272 num_before = len(list(glance.images.list()))
273 glance.images.delete(image)
274
275@@ -254,6 +279,8 @@
276
277 def create_instance(self, nova, image_name, instance_name, flavor):
278 """Create the specified instance."""
279+ self.log.debug('Creating instance '
280+ '({}|{}|{})'.format(instance_name, image_name, flavor))
281 image = nova.images.find(name=image_name)
282 flavor = nova.flavors.find(name=flavor)
283 instance = nova.servers.create(name=instance_name, image=image,
284@@ -276,6 +303,11 @@
285
286 def delete_instance(self, nova, instance):
287 """Delete the specified instance."""
288+
289+ # /!\ DEPRECATION WARNING
290+ self.log.warn('/!\\ DEPRECATION WARNING: use '
291+ 'delete_resource instead of delete_instance.')
292+ self.log.debug('Deleting instance ({})...'.format(instance))
293 num_before = len(list(nova.servers.list()))
294 nova.servers.delete(instance)
295
296@@ -292,3 +324,90 @@
297 return False
298
299 return True
300+
301+ def create_or_get_keypair(self, nova, keypair_name="testkey"):
302+ """Create a new keypair, or return pointer if it already exists."""
303+ try:
304+ _keypair = nova.keypairs.get(keypair_name)
305+ self.log.debug('Keypair ({}) already exists, '
306+ 'using it.'.format(keypair_name))
307+ return _keypair
308+ except:
309+ self.log.debug('Keypair ({}) does not exist, '
310+ 'creating it.'.format(keypair_name))
311+
312+ _keypair = nova.keypairs.create(name=keypair_name)
313+ return _keypair
314+
315+ def delete_resource(self, resource, resource_id,
316+ msg="resource", max_wait=120):
317+ """Delete one openstack resource, such as one instance, keypair,
318+ image, volume, stack, etc., and confirm deletion within max wait time.
319+
320+ :param resource: pointer to os resource type, ex:glance_client.images
321+ :param resource_id: unique name or id for the openstack resource
322+ :param msg: text to identify purpose in logging
323+ :param max_wait: maximum wait time in seconds
324+ :returns: True if successful, otherwise False
325+ """
326+ num_before = len(list(resource.list()))
327+ resource.delete(resource_id)
328+
329+ tries = 0
330+ num_after = len(list(resource.list()))
331+ while num_after != (num_before - 1) and tries < (max_wait / 4):
332+ self.log.debug('{} delete check: '
333+ '{} [{}:{}] {}'.format(msg, tries,
334+ num_before,
335+ num_after,
336+ resource_id))
337+ time.sleep(4)
338+ num_after = len(list(resource.list()))
339+ tries += 1
340+
341+ self.log.debug('{}: expected, actual count = {}, '
342+ '{}'.format(msg, num_before - 1, num_after))
343+
344+ if num_after == (num_before - 1):
345+ return True
346+ else:
347+ self.log.error('{} delete timed out'.format(msg))
348+ return False
349+
350+ def resource_reaches_status(self, resource, resource_id,
351+ expected_stat='available',
352+ msg='resource', max_wait=120):
353+ """Wait for an openstack resources status to reach an
354+ expected status within a specified time. Useful to confirm that
355+ nova instances, cinder vols, snapshots, glance images, heat stacks
356+ and other resources eventually reach the expected status.
357+
358+ :param resource: pointer to os resource type, ex: heat_client.stacks
359+ :param resource_id: unique id for the openstack resource
360+ :param expected_stat: status to expect resource to reach
361+ :param msg: text to identify purpose in logging
362+ :param max_wait: maximum wait time in seconds
363+ :returns: True if successful, False if status is not reached
364+ """
365+
366+ tries = 0
367+ resource_stat = resource.get(resource_id).status
368+ while resource_stat != expected_stat and tries < (max_wait / 4):
369+ self.log.debug('{} status check: '
370+ '{} [{}:{}] {}'.format(msg, tries,
371+ resource_stat,
372+ expected_stat,
373+ resource_id))
374+ time.sleep(4)
375+ resource_stat = resource.get(resource_id).status
376+ tries += 1
377+
378+ self.log.debug('{}: expected, actual status = {}, '
379+ '{}'.format(msg, resource_stat, expected_stat))
380+
381+ if resource_stat == expected_stat:
382+ return True
383+ else:
384+ self.log.debug('{} never reached expected status: '
385+ '{}'.format(resource_id, expected_stat))
386+ return False
387
388=== modified file 'hooks/charmhelpers/contrib/openstack/context.py'
389--- hooks/charmhelpers/contrib/openstack/context.py 2015-04-16 21:35:02 +0000
390+++ hooks/charmhelpers/contrib/openstack/context.py 2015-06-19 16:15:40 +0000
391@@ -240,7 +240,7 @@
392 if self.relation_prefix:
393 password_setting = self.relation_prefix + '_password'
394
395- for rid in relation_ids('shared-db'):
396+ for rid in relation_ids(self.interfaces[0]):
397 for unit in related_units(rid):
398 rdata = relation_get(rid=rid, unit=unit)
399 host = rdata.get('db_host')
400
401=== modified file 'hooks/charmhelpers/contrib/openstack/ip.py'
402--- hooks/charmhelpers/contrib/openstack/ip.py 2015-02-26 10:11:26 +0000
403+++ hooks/charmhelpers/contrib/openstack/ip.py 2015-06-19 16:15:40 +0000
404@@ -17,6 +17,7 @@
405 from charmhelpers.core.hookenv import (
406 config,
407 unit_get,
408+ service_name,
409 )
410 from charmhelpers.contrib.network.ip import (
411 get_address_in_network,
412@@ -26,8 +27,6 @@
413 )
414 from charmhelpers.contrib.hahelpers.cluster import is_clustered
415
416-from functools import partial
417-
418 PUBLIC = 'public'
419 INTERNAL = 'int'
420 ADMIN = 'admin'
421@@ -35,15 +34,18 @@
422 ADDRESS_MAP = {
423 PUBLIC: {
424 'config': 'os-public-network',
425- 'fallback': 'public-address'
426+ 'fallback': 'public-address',
427+ 'override': 'os-public-hostname',
428 },
429 INTERNAL: {
430 'config': 'os-internal-network',
431- 'fallback': 'private-address'
432+ 'fallback': 'private-address',
433+ 'override': 'os-internal-hostname',
434 },
435 ADMIN: {
436 'config': 'os-admin-network',
437- 'fallback': 'private-address'
438+ 'fallback': 'private-address',
439+ 'override': 'os-admin-hostname',
440 }
441 }
442
443@@ -57,15 +59,50 @@
444 :param endpoint_type: str endpoint type to resolve.
445 :param returns: str base URL for services on the current service unit.
446 """
447- scheme = 'http'
448- if 'https' in configs.complete_contexts():
449- scheme = 'https'
450+ scheme = _get_scheme(configs)
451+
452 address = resolve_address(endpoint_type)
453 if is_ipv6(address):
454 address = "[{}]".format(address)
455+
456 return '%s://%s' % (scheme, address)
457
458
459+def _get_scheme(configs):
460+ """Returns the scheme to use for the url (either http or https)
461+ depending upon whether https is in the configs value.
462+
463+ :param configs: OSTemplateRenderer config templating object to inspect
464+ for a complete https context.
465+ :returns: either 'http' or 'https' depending on whether https is
466+ configured within the configs context.
467+ """
468+ scheme = 'http'
469+ if configs and 'https' in configs.complete_contexts():
470+ scheme = 'https'
471+ return scheme
472+
473+
474+def _get_address_override(endpoint_type=PUBLIC):
475+ """Returns any address overrides that the user has defined based on the
476+ endpoint type.
477+
478+ Note: this function allows for the service name to be inserted into the
479+ address if the user specifies {service_name}.somehost.org.
480+
481+ :param endpoint_type: the type of endpoint to retrieve the override
482+ value for.
483+ :returns: any endpoint address or hostname that the user has overridden
484+ or None if an override is not present.
485+ """
486+ override_key = ADDRESS_MAP[endpoint_type]['override']
487+ addr_override = config(override_key)
488+ if not addr_override:
489+ return None
490+ else:
491+ return addr_override.format(service_name=service_name())
492+
493+
494 def resolve_address(endpoint_type=PUBLIC):
495 """Return unit address depending on net config.
496
497@@ -77,7 +114,10 @@
498
499 :param endpoint_type: Network endpoing type
500 """
501- resolved_address = None
502+ resolved_address = _get_address_override(endpoint_type)
503+ if resolved_address:
504+ return resolved_address
505+
506 vips = config('vip')
507 if vips:
508 vips = vips.split()
509@@ -109,38 +149,3 @@
510 "clustered=%s)" % (net_type, clustered))
511
512 return resolved_address
513-
514-
515-def endpoint_url(configs, url_template, port, endpoint_type=PUBLIC,
516- override=None):
517- """Returns the correct endpoint URL to advertise to Keystone.
518-
519- This method provides the correct endpoint URL which should be advertised to
520- the keystone charm for endpoint creation. This method allows for the url to
521- be overridden to force a keystone endpoint to have specific URL for any of
522- the defined scopes (admin, internal, public).
523-
524- :param configs: OSTemplateRenderer config templating object to inspect
525- for a complete https context.
526- :param url_template: str format string for creating the url template. Only
527- two values will be passed - the scheme+hostname
528- returned by the canonical_url and the port.
529- :param endpoint_type: str endpoint type to resolve.
530- :param override: str the name of the config option which overrides the
531- endpoint URL defined by the charm itself. None will
532- disable any overrides (default).
533- """
534- if override:
535- # Return any user-defined overrides for the keystone endpoint URL.
536- user_value = config(override)
537- if user_value:
538- return user_value.strip()
539-
540- return url_template % (canonical_url(configs, endpoint_type), port)
541-
542-
543-public_endpoint = partial(endpoint_url, endpoint_type=PUBLIC)
544-
545-internal_endpoint = partial(endpoint_url, endpoint_type=INTERNAL)
546-
547-admin_endpoint = partial(endpoint_url, endpoint_type=ADMIN)
548
549=== modified file 'hooks/charmhelpers/contrib/openstack/neutron.py'
550--- hooks/charmhelpers/contrib/openstack/neutron.py 2015-04-16 20:24:28 +0000
551+++ hooks/charmhelpers/contrib/openstack/neutron.py 2015-06-19 16:15:40 +0000
552@@ -172,14 +172,16 @@
553 'services': ['calico-felix',
554 'bird',
555 'neutron-dhcp-agent',
556- 'nova-api-metadata'],
557+ 'nova-api-metadata',
558+ 'etcd'],
559 'packages': [[headers_package()] + determine_dkms_package(),
560 ['calico-compute',
561 'bird',
562 'neutron-dhcp-agent',
563- 'nova-api-metadata']],
564- 'server_packages': ['neutron-server', 'calico-control'],
565- 'server_services': ['neutron-server']
566+ 'nova-api-metadata',
567+ 'etcd']],
568+ 'server_packages': ['neutron-server', 'calico-control', 'etcd'],
569+ 'server_services': ['neutron-server', 'etcd']
570 },
571 'vsp': {
572 'config': '/etc/neutron/plugins/nuage/nuage_plugin.ini',
573@@ -256,11 +258,14 @@
574 def parse_mappings(mappings):
575 parsed = {}
576 if mappings:
577- mappings = mappings.split(' ')
578+ mappings = mappings.split()
579 for m in mappings:
580 p = m.partition(':')
581- if p[1] == ':':
582- parsed[p[0].strip()] = p[2].strip()
583+ key = p[0].strip()
584+ if p[1]:
585+ parsed[key] = p[2].strip()
586+ else:
587+ parsed[key] = ''
588
589 return parsed
590
591@@ -283,13 +288,13 @@
592 Returns dict of the form {bridge:port}.
593 """
594 _mappings = parse_mappings(mappings)
595- if not _mappings:
596+ if not _mappings or list(_mappings.values()) == ['']:
597 if not mappings:
598 return {}
599
600 # For backwards-compatibility we need to support port-only provided in
601 # config.
602- _mappings = {default_bridge: mappings.split(' ')[0]}
603+ _mappings = {default_bridge: mappings.split()[0]}
604
605 bridges = _mappings.keys()
606 ports = _mappings.values()
607@@ -309,6 +314,8 @@
608
609 Mappings must be a space-delimited list of provider:start:end mappings.
610
611+ The start:end range is optional and may be omitted.
612+
613 Returns dict of the form {provider: (start, end)}.
614 """
615 _mappings = parse_mappings(mappings)
616
617=== modified file 'hooks/charmhelpers/contrib/openstack/utils.py'
618--- hooks/charmhelpers/contrib/openstack/utils.py 2015-05-12 14:25:49 +0000
619+++ hooks/charmhelpers/contrib/openstack/utils.py 2015-06-19 16:15:40 +0000
620@@ -79,6 +79,7 @@
621 ('trusty', 'icehouse'),
622 ('utopic', 'juno'),
623 ('vivid', 'kilo'),
624+ ('wily', 'liberty'),
625 ])
626
627
628@@ -91,6 +92,7 @@
629 ('2014.1', 'icehouse'),
630 ('2014.2', 'juno'),
631 ('2015.1', 'kilo'),
632+ ('2015.2', 'liberty'),
633 ])
634
635 # The ugly duckling
636@@ -113,6 +115,7 @@
637 ('2.2.0', 'juno'),
638 ('2.2.1', 'kilo'),
639 ('2.2.2', 'kilo'),
640+ ('2.3.0', 'liberty'),
641 ])
642
643 DEFAULT_LOOPBACK_SIZE = '5G'
644@@ -321,6 +324,9 @@
645 'kilo': 'trusty-updates/kilo',
646 'kilo/updates': 'trusty-updates/kilo',
647 'kilo/proposed': 'trusty-proposed/kilo',
648+ 'liberty': 'trusty-updates/liberty',
649+ 'liberty/updates': 'trusty-updates/liberty',
650+ 'liberty/proposed': 'trusty-proposed/liberty',
651 }
652
653 try:
654@@ -549,6 +555,11 @@
655
656 pip_create_virtualenv(os.path.join(parent_dir, 'venv'))
657
658+ # Upgrade setuptools from default virtualenv version. The default version
659+ # in trusty breaks update.py in global requirements master branch.
660+ pip_install('setuptools', upgrade=True, proxy=http_proxy,
661+ venv=os.path.join(parent_dir, 'venv'))
662+
663 for p in projects['repositories']:
664 repo = p['repository']
665 branch = p['branch']
666@@ -610,24 +621,24 @@
667 else:
668 repo_dir = dest_dir
669
670+ venv = os.path.join(parent_dir, 'venv')
671+
672 if update_requirements:
673 if not requirements_dir:
674 error_out('requirements repo must be cloned before '
675 'updating from global requirements.')
676- _git_update_requirements(repo_dir, requirements_dir)
677+ _git_update_requirements(venv, repo_dir, requirements_dir)
678
679 juju_log('Installing git repo from dir: {}'.format(repo_dir))
680 if http_proxy:
681- pip_install(repo_dir, proxy=http_proxy,
682- venv=os.path.join(parent_dir, 'venv'))
683+ pip_install(repo_dir, proxy=http_proxy, venv=venv)
684 else:
685- pip_install(repo_dir,
686- venv=os.path.join(parent_dir, 'venv'))
687+ pip_install(repo_dir, venv=venv)
688
689 return repo_dir
690
691
692-def _git_update_requirements(package_dir, reqs_dir):
693+def _git_update_requirements(venv, package_dir, reqs_dir):
694 """
695 Update from global requirements.
696
697@@ -636,12 +647,14 @@
698 """
699 orig_dir = os.getcwd()
700 os.chdir(reqs_dir)
701- cmd = ['python', 'update.py', package_dir]
702+ python = os.path.join(venv, 'bin/python')
703+ cmd = [python, 'update.py', package_dir]
704 try:
705 subprocess.check_call(cmd)
706 except subprocess.CalledProcessError:
707 package = os.path.basename(package_dir)
708- error_out("Error updating {} from global-requirements.txt".format(package))
709+ error_out("Error updating {} from "
710+ "global-requirements.txt".format(package))
711 os.chdir(orig_dir)
712
713
714
715=== modified file 'hooks/charmhelpers/core/hookenv.py'
716--- hooks/charmhelpers/core/hookenv.py 2015-04-16 20:24:28 +0000
717+++ hooks/charmhelpers/core/hookenv.py 2015-06-19 16:15:40 +0000
718@@ -21,12 +21,14 @@
719 # Charm Helpers Developers <juju@lists.ubuntu.com>
720
721 from __future__ import print_function
722+from functools import wraps
723 import os
724 import json
725 import yaml
726 import subprocess
727 import sys
728 import errno
729+import tempfile
730 from subprocess import CalledProcessError
731
732 import six
733@@ -58,15 +60,17 @@
734
735 will cache the result of unit_get + 'test' for future calls.
736 """
737+ @wraps(func)
738 def wrapper(*args, **kwargs):
739 global cache
740 key = str((func, args, kwargs))
741 try:
742 return cache[key]
743 except KeyError:
744- res = func(*args, **kwargs)
745- cache[key] = res
746- return res
747+ pass # Drop out of the exception handler scope.
748+ res = func(*args, **kwargs)
749+ cache[key] = res
750+ return res
751 return wrapper
752
753
754@@ -178,7 +182,7 @@
755
756 def remote_unit():
757 """The remote unit for the current relation hook"""
758- return os.environ['JUJU_REMOTE_UNIT']
759+ return os.environ.get('JUJU_REMOTE_UNIT', None)
760
761
762 def service_name():
763@@ -250,6 +254,12 @@
764 except KeyError:
765 return (self._prev_dict or {})[key]
766
767+ def get(self, key, default=None):
768+ try:
769+ return self[key]
770+ except KeyError:
771+ return default
772+
773 def keys(self):
774 prev_keys = []
775 if self._prev_dict is not None:
776@@ -353,18 +363,49 @@
777 """Set relation information for the current unit"""
778 relation_settings = relation_settings if relation_settings else {}
779 relation_cmd_line = ['relation-set']
780+ accepts_file = "--file" in subprocess.check_output(
781+ relation_cmd_line + ["--help"], universal_newlines=True)
782 if relation_id is not None:
783 relation_cmd_line.extend(('-r', relation_id))
784- for k, v in (list(relation_settings.items()) + list(kwargs.items())):
785- if v is None:
786- relation_cmd_line.append('{}='.format(k))
787- else:
788- relation_cmd_line.append('{}={}'.format(k, v))
789- subprocess.check_call(relation_cmd_line)
790+ settings = relation_settings.copy()
791+ settings.update(kwargs)
792+ for key, value in settings.items():
793+ # Force value to be a string: it always should, but some call
794+ # sites pass in things like dicts or numbers.
795+ if value is not None:
796+ settings[key] = "{}".format(value)
797+ if accepts_file:
798+ # --file was introduced in Juju 1.23.2. Use it by default if
799+ # available, since otherwise we'll break if the relation data is
800+ # too big. Ideally we should tell relation-set to read the data from
801+ # stdin, but that feature is broken in 1.23.2: Bug #1454678.
802+ with tempfile.NamedTemporaryFile(delete=False) as settings_file:
803+ settings_file.write(yaml.safe_dump(settings).encode("utf-8"))
804+ subprocess.check_call(
805+ relation_cmd_line + ["--file", settings_file.name])
806+ os.remove(settings_file.name)
807+ else:
808+ for key, value in settings.items():
809+ if value is None:
810+ relation_cmd_line.append('{}='.format(key))
811+ else:
812+ relation_cmd_line.append('{}={}'.format(key, value))
813+ subprocess.check_call(relation_cmd_line)
814 # Flush cache of any relation-gets for local unit
815 flush(local_unit())
816
817
818+def relation_clear(r_id=None):
819+ ''' Clears any relation data already set on relation r_id '''
820+ settings = relation_get(rid=r_id,
821+ unit=local_unit())
822+ for setting in settings:
823+ if setting not in ['public-address', 'private-address']:
824+ settings[setting] = None
825+ relation_set(relation_id=r_id,
826+ **settings)
827+
828+
829 @cached
830 def relation_ids(reltype=None):
831 """A list of relation_ids"""
832@@ -509,6 +550,11 @@
833 return None
834
835
836+def unit_public_ip():
837+ """Get this unit's public IP address"""
838+ return unit_get('public-address')
839+
840+
841 def unit_private_ip():
842 """Get this unit's private IP address"""
843 return unit_get('private-address')
844@@ -605,3 +651,94 @@
845
846 The results set by action_set are preserved."""
847 subprocess.check_call(['action-fail', message])
848+
849+
850+def status_set(workload_state, message):
851+ """Set the workload state with a message
852+
853+ Use status-set to set the workload state with a message which is visible
854+ to the user via juju status. If the status-set command is not found then
855+ assume this is juju < 1.23 and juju-log the message unstead.
856+
857+ workload_state -- valid juju workload state.
858+ message -- status update message
859+ """
860+ valid_states = ['maintenance', 'blocked', 'waiting', 'active']
861+ if workload_state not in valid_states:
862+ raise ValueError(
863+ '{!r} is not a valid workload state'.format(workload_state)
864+ )
865+ cmd = ['status-set', workload_state, message]
866+ try:
867+ ret = subprocess.call(cmd)
868+ if ret == 0:
869+ return
870+ except OSError as e:
871+ if e.errno != errno.ENOENT:
872+ raise
873+ log_message = 'status-set failed: {} {}'.format(workload_state,
874+ message)
875+ log(log_message, level='INFO')
876+
877+
878+def status_get():
879+ """Retrieve the previously set juju workload state
880+
881+ If the status-set command is not found then assume this is juju < 1.23 and
882+ return 'unknown'
883+ """
884+ cmd = ['status-get']
885+ try:
886+ raw_status = subprocess.check_output(cmd, universal_newlines=True)
887+ status = raw_status.rstrip()
888+ return status
889+ except OSError as e:
890+ if e.errno == errno.ENOENT:
891+ return 'unknown'
892+ else:
893+ raise
894+
895+
896+def translate_exc(from_exc, to_exc):
897+ def inner_translate_exc1(f):
898+ def inner_translate_exc2(*args, **kwargs):
899+ try:
900+ return f(*args, **kwargs)
901+ except from_exc:
902+ raise to_exc
903+
904+ return inner_translate_exc2
905+
906+ return inner_translate_exc1
907+
908+
909+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
910+def is_leader():
911+ """Does the current unit hold the juju leadership
912+
913+ Uses juju to determine whether the current unit is the leader of its peers
914+ """
915+ cmd = ['is-leader', '--format=json']
916+ return json.loads(subprocess.check_output(cmd).decode('UTF-8'))
917+
918+
919+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
920+def leader_get(attribute=None):
921+ """Juju leader get value(s)"""
922+ cmd = ['leader-get', '--format=json'] + [attribute or '-']
923+ return json.loads(subprocess.check_output(cmd).decode('UTF-8'))
924+
925+
926+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
927+def leader_set(settings=None, **kwargs):
928+ """Juju leader set value(s)"""
929+ log("Juju leader-set '%s'" % (settings), level=DEBUG)
930+ cmd = ['leader-set']
931+ settings = settings or {}
932+ settings.update(kwargs)
933+ for k, v in settings.iteritems():
934+ if v is None:
935+ cmd.append('{}='.format(k))
936+ else:
937+ cmd.append('{}={}'.format(k, v))
938+ subprocess.check_call(cmd)
939
940=== modified file 'hooks/charmhelpers/core/host.py'
941--- hooks/charmhelpers/core/host.py 2015-04-16 20:24:28 +0000
942+++ hooks/charmhelpers/core/host.py 2015-06-19 16:15:40 +0000
943@@ -24,6 +24,7 @@
944 import os
945 import re
946 import pwd
947+import glob
948 import grp
949 import random
950 import string
951@@ -90,7 +91,7 @@
952 ['service', service_name, 'status'],
953 stderr=subprocess.STDOUT).decode('UTF-8')
954 except subprocess.CalledProcessError as e:
955- return 'unrecognized service' not in e.output
956+ return b'unrecognized service' not in e.output
957 else:
958 return True
959
960@@ -269,6 +270,21 @@
961 return None
962
963
964+def path_hash(path):
965+ """
966+ Generate a hash checksum of all files matching 'path'. Standard wildcards
967+ like '*' and '?' are supported, see documentation for the 'glob' module for
968+ more information.
969+
970+ :return: dict: A { filename: hash } dictionary for all matched files.
971+ Empty if none found.
972+ """
973+ return {
974+ filename: file_hash(filename)
975+ for filename in glob.iglob(path)
976+ }
977+
978+
979 def check_hash(path, checksum, hash_type='md5'):
980 """
981 Validate a file using a cryptographic checksum.
982@@ -296,23 +312,25 @@
983
984 @restart_on_change({
985 '/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ]
986+ '/etc/apache/sites-enabled/*': [ 'apache2' ]
987 })
988- def ceph_client_changed():
989+ def config_changed():
990 pass # your code here
991
992 In this example, the cinder-api and cinder-volume services
993 would be restarted if /etc/ceph/ceph.conf is changed by the
994- ceph_client_changed function.
995+ ceph_client_changed function. The apache2 service would be
996+ restarted if any file matching the pattern got changed, created
997+ or removed. Standard wildcards are supported, see documentation
998+ for the 'glob' module for more information.
999 """
1000 def wrap(f):
1001 def wrapped_f(*args, **kwargs):
1002- checksums = {}
1003- for path in restart_map:
1004- checksums[path] = file_hash(path)
1005+ checksums = {path: path_hash(path) for path in restart_map}
1006 f(*args, **kwargs)
1007 restarts = []
1008 for path in restart_map:
1009- if checksums[path] != file_hash(path):
1010+ if path_hash(path) != checksums[path]:
1011 restarts += restart_map[path]
1012 services_list = list(OrderedDict.fromkeys(restarts))
1013 if not stopstart:
1014
1015=== modified file 'hooks/charmhelpers/core/services/base.py'
1016--- hooks/charmhelpers/core/services/base.py 2015-01-26 09:46:38 +0000
1017+++ hooks/charmhelpers/core/services/base.py 2015-06-19 16:15:40 +0000
1018@@ -15,9 +15,9 @@
1019 # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1020
1021 import os
1022-import re
1023 import json
1024-from collections import Iterable
1025+from inspect import getargspec
1026+from collections import Iterable, OrderedDict
1027
1028 from charmhelpers.core import host
1029 from charmhelpers.core import hookenv
1030@@ -119,7 +119,7 @@
1031 """
1032 self._ready_file = os.path.join(hookenv.charm_dir(), 'READY-SERVICES.json')
1033 self._ready = None
1034- self.services = {}
1035+ self.services = OrderedDict()
1036 for service in services or []:
1037 service_name = service['service']
1038 self.services[service_name] = service
1039@@ -132,8 +132,8 @@
1040 if hook_name == 'stop':
1041 self.stop_services()
1042 else:
1043+ self.reconfigure_services()
1044 self.provide_data()
1045- self.reconfigure_services()
1046 cfg = hookenv.config()
1047 if cfg.implicit_save:
1048 cfg.save()
1049@@ -145,15 +145,36 @@
1050 A provider must have a `name` attribute, which indicates which relation
1051 to set data on, and a `provide_data()` method, which returns a dict of
1052 data to set.
1053+
1054+ The `provide_data()` method can optionally accept two parameters:
1055+
1056+ * ``remote_service`` The name of the remote service that the data will
1057+ be provided to. The `provide_data()` method will be called once
1058+ for each connected service (not unit). This allows the method to
1059+ tailor its data to the given service.
1060+ * ``service_ready`` Whether or not the service definition had all of
1061+ its requirements met, and thus the ``data_ready`` callbacks run.
1062+
1063+ Note that the ``provided_data`` methods are now called **after** the
1064+ ``data_ready`` callbacks are run. This gives the ``data_ready`` callbacks
1065+ a chance to generate any data necessary for the providing to the remote
1066+ services.
1067 """
1068- hook_name = hookenv.hook_name()
1069- for service in self.services.values():
1070+ for service_name, service in self.services.items():
1071+ service_ready = self.is_ready(service_name)
1072 for provider in service.get('provided_data', []):
1073- if re.match(r'{}-relation-(joined|changed)'.format(provider.name), hook_name):
1074- data = provider.provide_data()
1075- _ready = provider._is_ready(data) if hasattr(provider, '_is_ready') else data
1076- if _ready:
1077- hookenv.relation_set(None, data)
1078+ for relid in hookenv.relation_ids(provider.name):
1079+ units = hookenv.related_units(relid)
1080+ if not units:
1081+ continue
1082+ remote_service = units[0].split('/')[0]
1083+ argspec = getargspec(provider.provide_data)
1084+ if len(argspec.args) > 1:
1085+ data = provider.provide_data(remote_service, service_ready)
1086+ else:
1087+ data = provider.provide_data()
1088+ if data:
1089+ hookenv.relation_set(relid, data)
1090
1091 def reconfigure_services(self, *service_names):
1092 """
1093
1094=== modified file 'hooks/charmhelpers/fetch/__init__.py'
1095--- hooks/charmhelpers/fetch/__init__.py 2015-05-01 14:57:35 +0000
1096+++ hooks/charmhelpers/fetch/__init__.py 2015-06-19 16:15:40 +0000
1097@@ -158,7 +158,7 @@
1098
1099 def apt_cache(in_memory=True):
1100 """Build and return an apt cache"""
1101- import apt_pkg
1102+ from apt import apt_pkg
1103 apt_pkg.init()
1104 if in_memory:
1105 apt_pkg.config.set("Dir::Cache::pkgcache", "")
1106
1107=== modified file 'tests/charmhelpers/contrib/amulet/utils.py'
1108--- tests/charmhelpers/contrib/amulet/utils.py 2015-05-05 20:28:18 +0000
1109+++ tests/charmhelpers/contrib/amulet/utils.py 2015-06-19 16:15:40 +0000
1110@@ -15,13 +15,15 @@
1111 # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1112
1113 import ConfigParser
1114+import distro_info
1115 import io
1116 import logging
1117+import os
1118 import re
1119+import six
1120 import sys
1121 import time
1122-
1123-import six
1124+import urlparse
1125
1126
1127 class AmuletUtils(object):
1128@@ -33,6 +35,7 @@
1129
1130 def __init__(self, log_level=logging.ERROR):
1131 self.log = self.get_logger(level=log_level)
1132+ self.ubuntu_releases = self.get_ubuntu_releases()
1133
1134 def get_logger(self, name="amulet-logger", level=logging.DEBUG):
1135 """Get a logger object that will log to stdout."""
1136@@ -70,12 +73,44 @@
1137 else:
1138 return False
1139
1140+ def get_ubuntu_release_from_sentry(self, sentry_unit):
1141+ """Get Ubuntu release codename from sentry unit.
1142+
1143+ :param sentry_unit: amulet sentry/service unit pointer
1144+ :returns: list of strings - release codename, failure message
1145+ """
1146+ msg = None
1147+ cmd = 'lsb_release -cs'
1148+ release, code = sentry_unit.run(cmd)
1149+ if code == 0:
1150+ self.log.debug('{} lsb_release: {}'.format(
1151+ sentry_unit.info['unit_name'], release))
1152+ else:
1153+ msg = ('{} `{}` returned {} '
1154+ '{}'.format(sentry_unit.info['unit_name'],
1155+ cmd, release, code))
1156+ if release not in self.ubuntu_releases:
1157+ msg = ("Release ({}) not found in Ubuntu releases "
1158+ "({})".format(release, self.ubuntu_releases))
1159+ return release, msg
1160+
1161 def validate_services(self, commands):
1162- """Validate services.
1163-
1164- Verify the specified services are running on the corresponding
1165+ """Validate that lists of commands succeed on service units. Can be
1166+ used to verify system services are running on the corresponding
1167 service units.
1168- """
1169+
1170+ :param commands: dict with sentry keys and arbitrary command list vals
1171+ :returns: None if successful, Failure string message otherwise
1172+ """
1173+ self.log.debug('Checking status of system services...')
1174+
1175+ # /!\ DEPRECATION WARNING (beisner):
1176+ # New and existing tests should be rewritten to use
1177+ # validate_services_by_name() as it is aware of init systems.
1178+ self.log.warn('/!\\ DEPRECATION WARNING: use '
1179+ 'validate_services_by_name instead of validate_services '
1180+ 'due to init system differences.')
1181+
1182 for k, v in six.iteritems(commands):
1183 for cmd in v:
1184 output, code = k.run(cmd)
1185@@ -86,6 +121,41 @@
1186 return "command `{}` returned {}".format(cmd, str(code))
1187 return None
1188
1189+ def validate_services_by_name(self, sentry_services):
1190+ """Validate system service status by service name, automatically
1191+ detecting init system based on Ubuntu release codename.
1192+
1193+ :param sentry_services: dict with sentry keys and svc list values
1194+ :returns: None if successful, Failure string message otherwise
1195+ """
1196+ self.log.debug('Checking status of system services...')
1197+
1198+ # Point at which systemd became a thing
1199+ systemd_switch = self.ubuntu_releases.index('vivid')
1200+
1201+ for sentry_unit, services_list in six.iteritems(sentry_services):
1202+ # Get lsb_release codename from unit
1203+ release, ret = self.get_ubuntu_release_from_sentry(sentry_unit)
1204+ if ret:
1205+ return ret
1206+
1207+ for service_name in services_list:
1208+ if (self.ubuntu_releases.index(release) >= systemd_switch or
1209+ service_name == "rabbitmq-server"):
1210+ # init is systemd
1211+ cmd = 'sudo service {} status'.format(service_name)
1212+ elif self.ubuntu_releases.index(release) < systemd_switch:
1213+ # init is upstart
1214+ cmd = 'sudo status {}'.format(service_name)
1215+
1216+ output, code = sentry_unit.run(cmd)
1217+ self.log.debug('{} `{}` returned '
1218+ '{}'.format(sentry_unit.info['unit_name'],
1219+ cmd, code))
1220+ if code != 0:
1221+ return "command `{}` returned {}".format(cmd, str(code))
1222+ return None
1223+
1224 def _get_config(self, unit, filename):
1225 """Get a ConfigParser object for parsing a unit's config file."""
1226 file_contents = unit.file_contents(filename)
1227@@ -104,6 +174,9 @@
1228 Verify that the specified section of the config file contains
1229 the expected option key:value pairs.
1230 """
1231+ self.log.debug('Validating config file data ({} in {} on {})'
1232+ '...'.format(section, config_file,
1233+ sentry_unit.info['unit_name']))
1234 config = self._get_config(sentry_unit, config_file)
1235
1236 if section != 'DEFAULT' and not config.has_section(section):
1237@@ -321,3 +394,15 @@
1238
1239 def endpoint_error(self, name, data):
1240 return 'unexpected endpoint data in {} - {}'.format(name, data)
1241+
1242+ def get_ubuntu_releases(self):
1243+ """Return a list of all Ubuntu releases in order of release."""
1244+ _d = distro_info.UbuntuDistroInfo()
1245+ _release_list = _d.all
1246+ self.log.debug('Ubuntu release list: {}'.format(_release_list))
1247+ return _release_list
1248+
1249+ def file_to_url(self, file_rel_path):
1250+ """Convert a relative file path to a file URL."""
1251+ _abs_path = os.path.abspath(file_rel_path)
1252+ return urlparse.urlparse(_abs_path, scheme='file').geturl()
1253
1254=== modified file 'tests/charmhelpers/contrib/openstack/amulet/deployment.py'
1255--- tests/charmhelpers/contrib/openstack/amulet/deployment.py 2015-05-05 20:28:18 +0000
1256+++ tests/charmhelpers/contrib/openstack/amulet/deployment.py 2015-06-19 16:15:40 +0000
1257@@ -110,7 +110,8 @@
1258 (self.precise_essex, self.precise_folsom, self.precise_grizzly,
1259 self.precise_havana, self.precise_icehouse,
1260 self.trusty_icehouse, self.trusty_juno, self.utopic_juno,
1261- self.trusty_kilo, self.vivid_kilo) = range(10)
1262+ self.trusty_kilo, self.vivid_kilo, self.trusty_liberty,
1263+ self.wily_liberty) = range(12)
1264
1265 releases = {
1266 ('precise', None): self.precise_essex,
1267@@ -121,8 +122,10 @@
1268 ('trusty', None): self.trusty_icehouse,
1269 ('trusty', 'cloud:trusty-juno'): self.trusty_juno,
1270 ('trusty', 'cloud:trusty-kilo'): self.trusty_kilo,
1271+ ('trusty', 'cloud:trusty-liberty'): self.trusty_liberty,
1272 ('utopic', None): self.utopic_juno,
1273- ('vivid', None): self.vivid_kilo}
1274+ ('vivid', None): self.vivid_kilo,
1275+ ('wily', None): self.wily_liberty}
1276 return releases[(self.series, self.openstack)]
1277
1278 def _get_openstack_release_string(self):
1279@@ -138,6 +141,7 @@
1280 ('trusty', 'icehouse'),
1281 ('utopic', 'juno'),
1282 ('vivid', 'kilo'),
1283+ ('wily', 'liberty'),
1284 ])
1285 if self.openstack:
1286 os_origin = self.openstack.split(':')[1]
1287
1288=== modified file 'tests/charmhelpers/contrib/openstack/amulet/utils.py'
1289--- tests/charmhelpers/contrib/openstack/amulet/utils.py 2015-02-10 18:50:39 +0000
1290+++ tests/charmhelpers/contrib/openstack/amulet/utils.py 2015-06-19 16:15:40 +0000
1291@@ -16,15 +16,15 @@
1292
1293 import logging
1294 import os
1295+import six
1296 import time
1297 import urllib
1298
1299 import glanceclient.v1.client as glance_client
1300+import heatclient.v1.client as heat_client
1301 import keystoneclient.v2_0 as keystone_client
1302 import novaclient.v1_1.client as nova_client
1303
1304-import six
1305-
1306 from charmhelpers.contrib.amulet.utils import (
1307 AmuletUtils
1308 )
1309@@ -37,7 +37,7 @@
1310 """OpenStack amulet utilities.
1311
1312 This class inherits from AmuletUtils and has additional support
1313- that is specifically for use by OpenStack charms.
1314+ that is specifically for use by OpenStack charm tests.
1315 """
1316
1317 def __init__(self, log_level=ERROR):
1318@@ -51,6 +51,8 @@
1319 Validate actual endpoint data vs expected endpoint data. The ports
1320 are used to find the matching endpoint.
1321 """
1322+ self.log.debug('Validating endpoint data...')
1323+ self.log.debug('actual: {}'.format(repr(endpoints)))
1324 found = False
1325 for ep in endpoints:
1326 self.log.debug('endpoint: {}'.format(repr(ep)))
1327@@ -77,6 +79,7 @@
1328 Validate a list of actual service catalog endpoints vs a list of
1329 expected service catalog endpoints.
1330 """
1331+ self.log.debug('Validating service catalog endpoint data...')
1332 self.log.debug('actual: {}'.format(repr(actual)))
1333 for k, v in six.iteritems(expected):
1334 if k in actual:
1335@@ -93,6 +96,7 @@
1336 Validate a list of actual tenant data vs list of expected tenant
1337 data.
1338 """
1339+ self.log.debug('Validating tenant data...')
1340 self.log.debug('actual: {}'.format(repr(actual)))
1341 for e in expected:
1342 found = False
1343@@ -114,6 +118,7 @@
1344 Validate a list of actual role data vs a list of expected role
1345 data.
1346 """
1347+ self.log.debug('Validating role data...')
1348 self.log.debug('actual: {}'.format(repr(actual)))
1349 for e in expected:
1350 found = False
1351@@ -134,6 +139,7 @@
1352 Validate a list of actual user data vs a list of expected user
1353 data.
1354 """
1355+ self.log.debug('Validating user data...')
1356 self.log.debug('actual: {}'.format(repr(actual)))
1357 for e in expected:
1358 found = False
1359@@ -155,17 +161,20 @@
1360
1361 Validate a list of actual flavors vs a list of expected flavors.
1362 """
1363+ self.log.debug('Validating flavor data...')
1364 self.log.debug('actual: {}'.format(repr(actual)))
1365 act = [a.name for a in actual]
1366 return self._validate_list_data(expected, act)
1367
1368 def tenant_exists(self, keystone, tenant):
1369 """Return True if tenant exists."""
1370+ self.log.debug('Checking if tenant exists ({})...'.format(tenant))
1371 return tenant in [t.name for t in keystone.tenants.list()]
1372
1373 def authenticate_keystone_admin(self, keystone_sentry, user, password,
1374 tenant):
1375 """Authenticates admin user with the keystone admin endpoint."""
1376+ self.log.debug('Authenticating keystone admin...')
1377 unit = keystone_sentry
1378 service_ip = unit.relation('shared-db',
1379 'mysql:shared-db')['private-address']
1380@@ -175,6 +184,7 @@
1381
1382 def authenticate_keystone_user(self, keystone, user, password, tenant):
1383 """Authenticates a regular user with the keystone public endpoint."""
1384+ self.log.debug('Authenticating keystone user ({})...'.format(user))
1385 ep = keystone.service_catalog.url_for(service_type='identity',
1386 endpoint_type='publicURL')
1387 return keystone_client.Client(username=user, password=password,
1388@@ -182,12 +192,21 @@
1389
1390 def authenticate_glance_admin(self, keystone):
1391 """Authenticates admin user with glance."""
1392+ self.log.debug('Authenticating glance admin...')
1393 ep = keystone.service_catalog.url_for(service_type='image',
1394 endpoint_type='adminURL')
1395 return glance_client.Client(ep, token=keystone.auth_token)
1396
1397+ def authenticate_heat_admin(self, keystone):
1398+ """Authenticates the admin user with heat."""
1399+ self.log.debug('Authenticating heat admin...')
1400+ ep = keystone.service_catalog.url_for(service_type='orchestration',
1401+ endpoint_type='publicURL')
1402+ return heat_client.Client(endpoint=ep, token=keystone.auth_token)
1403+
1404 def authenticate_nova_user(self, keystone, user, password, tenant):
1405 """Authenticates a regular user with nova-api."""
1406+ self.log.debug('Authenticating nova user ({})...'.format(user))
1407 ep = keystone.service_catalog.url_for(service_type='identity',
1408 endpoint_type='publicURL')
1409 return nova_client.Client(username=user, api_key=password,
1410@@ -195,6 +214,7 @@
1411
1412 def create_cirros_image(self, glance, image_name):
1413 """Download the latest cirros image and upload it to glance."""
1414+ self.log.debug('Creating glance image ({})...'.format(image_name))
1415 http_proxy = os.getenv('AMULET_HTTP_PROXY')
1416 self.log.debug('AMULET_HTTP_PROXY: {}'.format(http_proxy))
1417 if http_proxy:
1418@@ -235,6 +255,11 @@
1419
1420 def delete_image(self, glance, image):
1421 """Delete the specified image."""
1422+
1423+ # /!\ DEPRECATION WARNING
1424+ self.log.warn('/!\\ DEPRECATION WARNING: use '
1425+ 'delete_resource instead of delete_image.')
1426+ self.log.debug('Deleting glance image ({})...'.format(image))
1427 num_before = len(list(glance.images.list()))
1428 glance.images.delete(image)
1429
1430@@ -254,6 +279,8 @@
1431
1432 def create_instance(self, nova, image_name, instance_name, flavor):
1433 """Create the specified instance."""
1434+ self.log.debug('Creating instance '
1435+ '({}|{}|{})'.format(instance_name, image_name, flavor))
1436 image = nova.images.find(name=image_name)
1437 flavor = nova.flavors.find(name=flavor)
1438 instance = nova.servers.create(name=instance_name, image=image,
1439@@ -276,6 +303,11 @@
1440
1441 def delete_instance(self, nova, instance):
1442 """Delete the specified instance."""
1443+
1444+ # /!\ DEPRECATION WARNING
1445+ self.log.warn('/!\\ DEPRECATION WARNING: use '
1446+ 'delete_resource instead of delete_instance.')
1447+ self.log.debug('Deleting instance ({})...'.format(instance))
1448 num_before = len(list(nova.servers.list()))
1449 nova.servers.delete(instance)
1450
1451@@ -292,3 +324,90 @@
1452 return False
1453
1454 return True
1455+
1456+ def create_or_get_keypair(self, nova, keypair_name="testkey"):
1457+ """Create a new keypair, or return pointer if it already exists."""
1458+ try:
1459+ _keypair = nova.keypairs.get(keypair_name)
1460+ self.log.debug('Keypair ({}) already exists, '
1461+ 'using it.'.format(keypair_name))
1462+ return _keypair
1463+ except:
1464+ self.log.debug('Keypair ({}) does not exist, '
1465+ 'creating it.'.format(keypair_name))
1466+
1467+ _keypair = nova.keypairs.create(name=keypair_name)
1468+ return _keypair
1469+
1470+ def delete_resource(self, resource, resource_id,
1471+ msg="resource", max_wait=120):
1472+ """Delete one openstack resource, such as one instance, keypair,
1473+ image, volume, stack, etc., and confirm deletion within max wait time.
1474+
1475+ :param resource: pointer to os resource type, ex:glance_client.images
1476+ :param resource_id: unique name or id for the openstack resource
1477+ :param msg: text to identify purpose in logging
1478+ :param max_wait: maximum wait time in seconds
1479+ :returns: True if successful, otherwise False
1480+ """
1481+ num_before = len(list(resource.list()))
1482+ resource.delete(resource_id)
1483+
1484+ tries = 0
1485+ num_after = len(list(resource.list()))
1486+ while num_after != (num_before - 1) and tries < (max_wait / 4):
1487+ self.log.debug('{} delete check: '
1488+ '{} [{}:{}] {}'.format(msg, tries,
1489+ num_before,
1490+ num_after,
1491+ resource_id))
1492+ time.sleep(4)
1493+ num_after = len(list(resource.list()))
1494+ tries += 1
1495+
1496+ self.log.debug('{}: expected, actual count = {}, '
1497+ '{}'.format(msg, num_before - 1, num_after))
1498+
1499+ if num_after == (num_before - 1):
1500+ return True
1501+ else:
1502+ self.log.error('{} delete timed out'.format(msg))
1503+ return False
1504+
1505+ def resource_reaches_status(self, resource, resource_id,
1506+ expected_stat='available',
1507+ msg='resource', max_wait=120):
1508+ """Wait for an openstack resources status to reach an
1509+ expected status within a specified time. Useful to confirm that
1510+ nova instances, cinder vols, snapshots, glance images, heat stacks
1511+ and other resources eventually reach the expected status.
1512+
1513+ :param resource: pointer to os resource type, ex: heat_client.stacks
1514+ :param resource_id: unique id for the openstack resource
1515+ :param expected_stat: status to expect resource to reach
1516+ :param msg: text to identify purpose in logging
1517+ :param max_wait: maximum wait time in seconds
1518+ :returns: True if successful, False if status is not reached
1519+ """
1520+
1521+ tries = 0
1522+ resource_stat = resource.get(resource_id).status
1523+ while resource_stat != expected_stat and tries < (max_wait / 4):
1524+ self.log.debug('{} status check: '
1525+ '{} [{}:{}] {}'.format(msg, tries,
1526+ resource_stat,
1527+ expected_stat,
1528+ resource_id))
1529+ time.sleep(4)
1530+ resource_stat = resource.get(resource_id).status
1531+ tries += 1
1532+
1533+ self.log.debug('{}: expected, actual status = {}, '
1534+ '{}'.format(msg, resource_stat, expected_stat))
1535+
1536+ if resource_stat == expected_stat:
1537+ return True
1538+ else:
1539+ self.log.debug('{} never reached expected status: '
1540+ '{}'.format(resource_id, expected_stat))
1541+ return False
1542
1543=== modified file 'unit_tests/test_horizon_hooks.py'
1544--- unit_tests/test_horizon_hooks.py 2015-04-07 13:58:41 +0000
1545+++ unit_tests/test_horizon_hooks.py 2015-06-19 16:15:40 +0000
1546@@ -129,7 +129,7 @@
1547 self.apt_install.assert_called_with(['foo', 'bar'], fatal=True)
1548 self.git_install.assert_called_with(projects_yaml)
1549
1550- @patch('charmhelpers.core.host.file_hash')
1551+ @patch('charmhelpers.core.host.path_hash')
1552 @patch('charmhelpers.core.host.service')
1553 @patch.object(utils, 'git_install_requested')
1554 def test_upgrade_charm_hook(self, _git_requested, _service, _hash):

Subscribers

People subscribed via source and target branches