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
=== modified file 'hooks/charmhelpers/contrib/hahelpers/cluster.py'
--- hooks/charmhelpers/contrib/hahelpers/cluster.py 2015-02-26 10:11:26 +0000
+++ hooks/charmhelpers/contrib/hahelpers/cluster.py 2015-06-19 16:15:40 +0000
@@ -44,6 +44,7 @@
44 ERROR,44 ERROR,
45 WARNING,45 WARNING,
46 unit_get,46 unit_get,
47 is_leader as juju_is_leader
47)48)
48from charmhelpers.core.decorators import (49from charmhelpers.core.decorators import (
49 retry_on_exception,50 retry_on_exception,
@@ -52,6 +53,8 @@
52 bool_from_string,53 bool_from_string,
53)54)
5455
56DC_RESOURCE_NAME = 'DC'
57
5558
56class HAIncompleteConfig(Exception):59class HAIncompleteConfig(Exception):
57 pass60 pass
@@ -61,17 +64,30 @@
61 pass64 pass
6265
6366
67class CRMDCNotFound(Exception):
68 pass
69
70
64def is_elected_leader(resource):71def is_elected_leader(resource):
65 """72 """
66 Returns True if the charm executing this is the elected cluster leader.73 Returns True if the charm executing this is the elected cluster leader.
6774
68 It relies on two mechanisms to determine leadership:75 It relies on two mechanisms to determine leadership:
69 1. If the charm is part of a corosync cluster, call corosync to76 1. If juju is sufficiently new and leadership election is supported,
77 the is_leader command will be used.
78 2. If the charm is part of a corosync cluster, call corosync to
70 determine leadership.79 determine leadership.
71 2. If the charm is not part of a corosync cluster, the leader is80 3. If the charm is not part of a corosync cluster, the leader is
72 determined as being "the alive unit with the lowest unit numer". In81 determined as being "the alive unit with the lowest unit numer". In
73 other words, the oldest surviving unit.82 other words, the oldest surviving unit.
74 """83 """
84 try:
85 return juju_is_leader()
86 except NotImplementedError:
87 log('Juju leadership election feature not enabled'
88 ', using fallback support',
89 level=WARNING)
90
75 if is_clustered():91 if is_clustered():
76 if not is_crm_leader(resource):92 if not is_crm_leader(resource):
77 log('Deferring action to CRM leader.', level=INFO)93 log('Deferring action to CRM leader.', level=INFO)
@@ -95,7 +111,33 @@
95 return False111 return False
96112
97113
98@retry_on_exception(5, base_delay=2, exc_type=CRMResourceNotFound)114def is_crm_dc():
115 """
116 Determine leadership by querying the pacemaker Designated Controller
117 """
118 cmd = ['crm', 'status']
119 try:
120 status = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
121 if not isinstance(status, six.text_type):
122 status = six.text_type(status, "utf-8")
123 except subprocess.CalledProcessError as ex:
124 raise CRMDCNotFound(str(ex))
125
126 current_dc = ''
127 for line in status.split('\n'):
128 if line.startswith('Current DC'):
129 # Current DC: juju-lytrusty-machine-2 (168108163) - partition with quorum
130 current_dc = line.split(':')[1].split()[0]
131 if current_dc == get_unit_hostname():
132 return True
133 elif current_dc == 'NONE':
134 raise CRMDCNotFound('Current DC: NONE')
135
136 return False
137
138
139@retry_on_exception(5, base_delay=2,
140 exc_type=(CRMResourceNotFound, CRMDCNotFound))
99def is_crm_leader(resource, retry=False):141def is_crm_leader(resource, retry=False):
100 """142 """
101 Returns True if the charm calling this is the elected corosync leader,143 Returns True if the charm calling this is the elected corosync leader,
@@ -104,6 +146,8 @@
104 We allow this operation to be retried to avoid the possibility of getting a146 We allow this operation to be retried to avoid the possibility of getting a
105 false negative. See LP #1396246 for more info.147 false negative. See LP #1396246 for more info.
106 """148 """
149 if resource == DC_RESOURCE_NAME:
150 return is_crm_dc()
107 cmd = ['crm', 'resource', 'show', resource]151 cmd = ['crm', 'resource', 'show', resource]
108 try:152 try:
109 status = subprocess.check_output(cmd, stderr=subprocess.STDOUT)153 status = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
110154
=== modified file 'hooks/charmhelpers/contrib/openstack/amulet/deployment.py'
--- hooks/charmhelpers/contrib/openstack/amulet/deployment.py 2015-05-05 20:28:18 +0000
+++ hooks/charmhelpers/contrib/openstack/amulet/deployment.py 2015-06-19 16:15:40 +0000
@@ -110,7 +110,8 @@
110 (self.precise_essex, self.precise_folsom, self.precise_grizzly,110 (self.precise_essex, self.precise_folsom, self.precise_grizzly,
111 self.precise_havana, self.precise_icehouse,111 self.precise_havana, self.precise_icehouse,
112 self.trusty_icehouse, self.trusty_juno, self.utopic_juno,112 self.trusty_icehouse, self.trusty_juno, self.utopic_juno,
113 self.trusty_kilo, self.vivid_kilo) = range(10)113 self.trusty_kilo, self.vivid_kilo, self.trusty_liberty,
114 self.wily_liberty) = range(12)
114115
115 releases = {116 releases = {
116 ('precise', None): self.precise_essex,117 ('precise', None): self.precise_essex,
@@ -121,8 +122,10 @@
121 ('trusty', None): self.trusty_icehouse,122 ('trusty', None): self.trusty_icehouse,
122 ('trusty', 'cloud:trusty-juno'): self.trusty_juno,123 ('trusty', 'cloud:trusty-juno'): self.trusty_juno,
123 ('trusty', 'cloud:trusty-kilo'): self.trusty_kilo,124 ('trusty', 'cloud:trusty-kilo'): self.trusty_kilo,
125 ('trusty', 'cloud:trusty-liberty'): self.trusty_liberty,
124 ('utopic', None): self.utopic_juno,126 ('utopic', None): self.utopic_juno,
125 ('vivid', None): self.vivid_kilo}127 ('vivid', None): self.vivid_kilo,
128 ('wily', None): self.wily_liberty}
126 return releases[(self.series, self.openstack)]129 return releases[(self.series, self.openstack)]
127130
128 def _get_openstack_release_string(self):131 def _get_openstack_release_string(self):
@@ -138,6 +141,7 @@
138 ('trusty', 'icehouse'),141 ('trusty', 'icehouse'),
139 ('utopic', 'juno'),142 ('utopic', 'juno'),
140 ('vivid', 'kilo'),143 ('vivid', 'kilo'),
144 ('wily', 'liberty'),
141 ])145 ])
142 if self.openstack:146 if self.openstack:
143 os_origin = self.openstack.split(':')[1]147 os_origin = self.openstack.split(':')[1]
144148
=== modified file 'hooks/charmhelpers/contrib/openstack/amulet/utils.py'
--- hooks/charmhelpers/contrib/openstack/amulet/utils.py 2015-01-26 09:46:38 +0000
+++ hooks/charmhelpers/contrib/openstack/amulet/utils.py 2015-06-19 16:15:40 +0000
@@ -16,15 +16,15 @@
1616
17import logging17import logging
18import os18import os
19import six
19import time20import time
20import urllib21import urllib
2122
22import glanceclient.v1.client as glance_client23import glanceclient.v1.client as glance_client
24import heatclient.v1.client as heat_client
23import keystoneclient.v2_0 as keystone_client25import keystoneclient.v2_0 as keystone_client
24import novaclient.v1_1.client as nova_client26import novaclient.v1_1.client as nova_client
2527
26import six
27
28from charmhelpers.contrib.amulet.utils import (28from charmhelpers.contrib.amulet.utils import (
29 AmuletUtils29 AmuletUtils
30)30)
@@ -37,7 +37,7 @@
37 """OpenStack amulet utilities.37 """OpenStack amulet utilities.
3838
39 This class inherits from AmuletUtils and has additional support39 This class inherits from AmuletUtils and has additional support
40 that is specifically for use by OpenStack charms.40 that is specifically for use by OpenStack charm tests.
41 """41 """
4242
43 def __init__(self, log_level=ERROR):43 def __init__(self, log_level=ERROR):
@@ -51,6 +51,8 @@
51 Validate actual endpoint data vs expected endpoint data. The ports51 Validate actual endpoint data vs expected endpoint data. The ports
52 are used to find the matching endpoint.52 are used to find the matching endpoint.
53 """53 """
54 self.log.debug('Validating endpoint data...')
55 self.log.debug('actual: {}'.format(repr(endpoints)))
54 found = False56 found = False
55 for ep in endpoints:57 for ep in endpoints:
56 self.log.debug('endpoint: {}'.format(repr(ep)))58 self.log.debug('endpoint: {}'.format(repr(ep)))
@@ -77,6 +79,7 @@
77 Validate a list of actual service catalog endpoints vs a list of79 Validate a list of actual service catalog endpoints vs a list of
78 expected service catalog endpoints.80 expected service catalog endpoints.
79 """81 """
82 self.log.debug('Validating service catalog endpoint data...')
80 self.log.debug('actual: {}'.format(repr(actual)))83 self.log.debug('actual: {}'.format(repr(actual)))
81 for k, v in six.iteritems(expected):84 for k, v in six.iteritems(expected):
82 if k in actual:85 if k in actual:
@@ -93,6 +96,7 @@
93 Validate a list of actual tenant data vs list of expected tenant96 Validate a list of actual tenant data vs list of expected tenant
94 data.97 data.
95 """98 """
99 self.log.debug('Validating tenant data...')
96 self.log.debug('actual: {}'.format(repr(actual)))100 self.log.debug('actual: {}'.format(repr(actual)))
97 for e in expected:101 for e in expected:
98 found = False102 found = False
@@ -114,6 +118,7 @@
114 Validate a list of actual role data vs a list of expected role118 Validate a list of actual role data vs a list of expected role
115 data.119 data.
116 """120 """
121 self.log.debug('Validating role data...')
117 self.log.debug('actual: {}'.format(repr(actual)))122 self.log.debug('actual: {}'.format(repr(actual)))
118 for e in expected:123 for e in expected:
119 found = False124 found = False
@@ -134,6 +139,7 @@
134 Validate a list of actual user data vs a list of expected user139 Validate a list of actual user data vs a list of expected user
135 data.140 data.
136 """141 """
142 self.log.debug('Validating user data...')
137 self.log.debug('actual: {}'.format(repr(actual)))143 self.log.debug('actual: {}'.format(repr(actual)))
138 for e in expected:144 for e in expected:
139 found = False145 found = False
@@ -155,17 +161,20 @@
155161
156 Validate a list of actual flavors vs a list of expected flavors.162 Validate a list of actual flavors vs a list of expected flavors.
157 """163 """
164 self.log.debug('Validating flavor data...')
158 self.log.debug('actual: {}'.format(repr(actual)))165 self.log.debug('actual: {}'.format(repr(actual)))
159 act = [a.name for a in actual]166 act = [a.name for a in actual]
160 return self._validate_list_data(expected, act)167 return self._validate_list_data(expected, act)
161168
162 def tenant_exists(self, keystone, tenant):169 def tenant_exists(self, keystone, tenant):
163 """Return True if tenant exists."""170 """Return True if tenant exists."""
171 self.log.debug('Checking if tenant exists ({})...'.format(tenant))
164 return tenant in [t.name for t in keystone.tenants.list()]172 return tenant in [t.name for t in keystone.tenants.list()]
165173
166 def authenticate_keystone_admin(self, keystone_sentry, user, password,174 def authenticate_keystone_admin(self, keystone_sentry, user, password,
167 tenant):175 tenant):
168 """Authenticates admin user with the keystone admin endpoint."""176 """Authenticates admin user with the keystone admin endpoint."""
177 self.log.debug('Authenticating keystone admin...')
169 unit = keystone_sentry178 unit = keystone_sentry
170 service_ip = unit.relation('shared-db',179 service_ip = unit.relation('shared-db',
171 'mysql:shared-db')['private-address']180 'mysql:shared-db')['private-address']
@@ -175,6 +184,7 @@
175184
176 def authenticate_keystone_user(self, keystone, user, password, tenant):185 def authenticate_keystone_user(self, keystone, user, password, tenant):
177 """Authenticates a regular user with the keystone public endpoint."""186 """Authenticates a regular user with the keystone public endpoint."""
187 self.log.debug('Authenticating keystone user ({})...'.format(user))
178 ep = keystone.service_catalog.url_for(service_type='identity',188 ep = keystone.service_catalog.url_for(service_type='identity',
179 endpoint_type='publicURL')189 endpoint_type='publicURL')
180 return keystone_client.Client(username=user, password=password,190 return keystone_client.Client(username=user, password=password,
@@ -182,12 +192,21 @@
182192
183 def authenticate_glance_admin(self, keystone):193 def authenticate_glance_admin(self, keystone):
184 """Authenticates admin user with glance."""194 """Authenticates admin user with glance."""
195 self.log.debug('Authenticating glance admin...')
185 ep = keystone.service_catalog.url_for(service_type='image',196 ep = keystone.service_catalog.url_for(service_type='image',
186 endpoint_type='adminURL')197 endpoint_type='adminURL')
187 return glance_client.Client(ep, token=keystone.auth_token)198 return glance_client.Client(ep, token=keystone.auth_token)
188199
200 def authenticate_heat_admin(self, keystone):
201 """Authenticates the admin user with heat."""
202 self.log.debug('Authenticating heat admin...')
203 ep = keystone.service_catalog.url_for(service_type='orchestration',
204 endpoint_type='publicURL')
205 return heat_client.Client(endpoint=ep, token=keystone.auth_token)
206
189 def authenticate_nova_user(self, keystone, user, password, tenant):207 def authenticate_nova_user(self, keystone, user, password, tenant):
190 """Authenticates a regular user with nova-api."""208 """Authenticates a regular user with nova-api."""
209 self.log.debug('Authenticating nova user ({})...'.format(user))
191 ep = keystone.service_catalog.url_for(service_type='identity',210 ep = keystone.service_catalog.url_for(service_type='identity',
192 endpoint_type='publicURL')211 endpoint_type='publicURL')
193 return nova_client.Client(username=user, api_key=password,212 return nova_client.Client(username=user, api_key=password,
@@ -195,6 +214,7 @@
195214
196 def create_cirros_image(self, glance, image_name):215 def create_cirros_image(self, glance, image_name):
197 """Download the latest cirros image and upload it to glance."""216 """Download the latest cirros image and upload it to glance."""
217 self.log.debug('Creating glance image ({})...'.format(image_name))
198 http_proxy = os.getenv('AMULET_HTTP_PROXY')218 http_proxy = os.getenv('AMULET_HTTP_PROXY')
199 self.log.debug('AMULET_HTTP_PROXY: {}'.format(http_proxy))219 self.log.debug('AMULET_HTTP_PROXY: {}'.format(http_proxy))
200 if http_proxy:220 if http_proxy:
@@ -235,6 +255,11 @@
235255
236 def delete_image(self, glance, image):256 def delete_image(self, glance, image):
237 """Delete the specified image."""257 """Delete the specified image."""
258
259 # /!\ DEPRECATION WARNING
260 self.log.warn('/!\\ DEPRECATION WARNING: use '
261 'delete_resource instead of delete_image.')
262 self.log.debug('Deleting glance image ({})...'.format(image))
238 num_before = len(list(glance.images.list()))263 num_before = len(list(glance.images.list()))
239 glance.images.delete(image)264 glance.images.delete(image)
240265
@@ -254,6 +279,8 @@
254279
255 def create_instance(self, nova, image_name, instance_name, flavor):280 def create_instance(self, nova, image_name, instance_name, flavor):
256 """Create the specified instance."""281 """Create the specified instance."""
282 self.log.debug('Creating instance '
283 '({}|{}|{})'.format(instance_name, image_name, flavor))
257 image = nova.images.find(name=image_name)284 image = nova.images.find(name=image_name)
258 flavor = nova.flavors.find(name=flavor)285 flavor = nova.flavors.find(name=flavor)
259 instance = nova.servers.create(name=instance_name, image=image,286 instance = nova.servers.create(name=instance_name, image=image,
@@ -276,6 +303,11 @@
276303
277 def delete_instance(self, nova, instance):304 def delete_instance(self, nova, instance):
278 """Delete the specified instance."""305 """Delete the specified instance."""
306
307 # /!\ DEPRECATION WARNING
308 self.log.warn('/!\\ DEPRECATION WARNING: use '
309 'delete_resource instead of delete_instance.')
310 self.log.debug('Deleting instance ({})...'.format(instance))
279 num_before = len(list(nova.servers.list()))311 num_before = len(list(nova.servers.list()))
280 nova.servers.delete(instance)312 nova.servers.delete(instance)
281313
@@ -292,3 +324,90 @@
292 return False324 return False
293325
294 return True326 return True
327
328 def create_or_get_keypair(self, nova, keypair_name="testkey"):
329 """Create a new keypair, or return pointer if it already exists."""
330 try:
331 _keypair = nova.keypairs.get(keypair_name)
332 self.log.debug('Keypair ({}) already exists, '
333 'using it.'.format(keypair_name))
334 return _keypair
335 except:
336 self.log.debug('Keypair ({}) does not exist, '
337 'creating it.'.format(keypair_name))
338
339 _keypair = nova.keypairs.create(name=keypair_name)
340 return _keypair
341
342 def delete_resource(self, resource, resource_id,
343 msg="resource", max_wait=120):
344 """Delete one openstack resource, such as one instance, keypair,
345 image, volume, stack, etc., and confirm deletion within max wait time.
346
347 :param resource: pointer to os resource type, ex:glance_client.images
348 :param resource_id: unique name or id for the openstack resource
349 :param msg: text to identify purpose in logging
350 :param max_wait: maximum wait time in seconds
351 :returns: True if successful, otherwise False
352 """
353 num_before = len(list(resource.list()))
354 resource.delete(resource_id)
355
356 tries = 0
357 num_after = len(list(resource.list()))
358 while num_after != (num_before - 1) and tries < (max_wait / 4):
359 self.log.debug('{} delete check: '
360 '{} [{}:{}] {}'.format(msg, tries,
361 num_before,
362 num_after,
363 resource_id))
364 time.sleep(4)
365 num_after = len(list(resource.list()))
366 tries += 1
367
368 self.log.debug('{}: expected, actual count = {}, '
369 '{}'.format(msg, num_before - 1, num_after))
370
371 if num_after == (num_before - 1):
372 return True
373 else:
374 self.log.error('{} delete timed out'.format(msg))
375 return False
376
377 def resource_reaches_status(self, resource, resource_id,
378 expected_stat='available',
379 msg='resource', max_wait=120):
380 """Wait for an openstack resources status to reach an
381 expected status within a specified time. Useful to confirm that
382 nova instances, cinder vols, snapshots, glance images, heat stacks
383 and other resources eventually reach the expected status.
384
385 :param resource: pointer to os resource type, ex: heat_client.stacks
386 :param resource_id: unique id for the openstack resource
387 :param expected_stat: status to expect resource to reach
388 :param msg: text to identify purpose in logging
389 :param max_wait: maximum wait time in seconds
390 :returns: True if successful, False if status is not reached
391 """
392
393 tries = 0
394 resource_stat = resource.get(resource_id).status
395 while resource_stat != expected_stat and tries < (max_wait / 4):
396 self.log.debug('{} status check: '
397 '{} [{}:{}] {}'.format(msg, tries,
398 resource_stat,
399 expected_stat,
400 resource_id))
401 time.sleep(4)
402 resource_stat = resource.get(resource_id).status
403 tries += 1
404
405 self.log.debug('{}: expected, actual status = {}, '
406 '{}'.format(msg, resource_stat, expected_stat))
407
408 if resource_stat == expected_stat:
409 return True
410 else:
411 self.log.debug('{} never reached expected status: '
412 '{}'.format(resource_id, expected_stat))
413 return False
295414
=== modified file 'hooks/charmhelpers/contrib/openstack/context.py'
--- hooks/charmhelpers/contrib/openstack/context.py 2015-04-16 21:35:02 +0000
+++ hooks/charmhelpers/contrib/openstack/context.py 2015-06-19 16:15:40 +0000
@@ -240,7 +240,7 @@
240 if self.relation_prefix:240 if self.relation_prefix:
241 password_setting = self.relation_prefix + '_password'241 password_setting = self.relation_prefix + '_password'
242242
243 for rid in relation_ids('shared-db'):243 for rid in relation_ids(self.interfaces[0]):
244 for unit in related_units(rid):244 for unit in related_units(rid):
245 rdata = relation_get(rid=rid, unit=unit)245 rdata = relation_get(rid=rid, unit=unit)
246 host = rdata.get('db_host')246 host = rdata.get('db_host')
247247
=== modified file 'hooks/charmhelpers/contrib/openstack/ip.py'
--- hooks/charmhelpers/contrib/openstack/ip.py 2015-02-26 10:11:26 +0000
+++ hooks/charmhelpers/contrib/openstack/ip.py 2015-06-19 16:15:40 +0000
@@ -17,6 +17,7 @@
17from charmhelpers.core.hookenv import (17from charmhelpers.core.hookenv import (
18 config,18 config,
19 unit_get,19 unit_get,
20 service_name,
20)21)
21from charmhelpers.contrib.network.ip import (22from charmhelpers.contrib.network.ip import (
22 get_address_in_network,23 get_address_in_network,
@@ -26,8 +27,6 @@
26)27)
27from charmhelpers.contrib.hahelpers.cluster import is_clustered28from charmhelpers.contrib.hahelpers.cluster import is_clustered
2829
29from functools import partial
30
31PUBLIC = 'public'30PUBLIC = 'public'
32INTERNAL = 'int'31INTERNAL = 'int'
33ADMIN = 'admin'32ADMIN = 'admin'
@@ -35,15 +34,18 @@
35ADDRESS_MAP = {34ADDRESS_MAP = {
36 PUBLIC: {35 PUBLIC: {
37 'config': 'os-public-network',36 'config': 'os-public-network',
38 'fallback': 'public-address'37 'fallback': 'public-address',
38 'override': 'os-public-hostname',
39 },39 },
40 INTERNAL: {40 INTERNAL: {
41 'config': 'os-internal-network',41 'config': 'os-internal-network',
42 'fallback': 'private-address'42 'fallback': 'private-address',
43 'override': 'os-internal-hostname',
43 },44 },
44 ADMIN: {45 ADMIN: {
45 'config': 'os-admin-network',46 'config': 'os-admin-network',
46 'fallback': 'private-address'47 'fallback': 'private-address',
48 'override': 'os-admin-hostname',
47 }49 }
48}50}
4951
@@ -57,15 +59,50 @@
57 :param endpoint_type: str endpoint type to resolve.59 :param endpoint_type: str endpoint type to resolve.
58 :param returns: str base URL for services on the current service unit.60 :param returns: str base URL for services on the current service unit.
59 """61 """
60 scheme = 'http'62 scheme = _get_scheme(configs)
61 if 'https' in configs.complete_contexts():63
62 scheme = 'https'
63 address = resolve_address(endpoint_type)64 address = resolve_address(endpoint_type)
64 if is_ipv6(address):65 if is_ipv6(address):
65 address = "[{}]".format(address)66 address = "[{}]".format(address)
67
66 return '%s://%s' % (scheme, address)68 return '%s://%s' % (scheme, address)
6769
6870
71def _get_scheme(configs):
72 """Returns the scheme to use for the url (either http or https)
73 depending upon whether https is in the configs value.
74
75 :param configs: OSTemplateRenderer config templating object to inspect
76 for a complete https context.
77 :returns: either 'http' or 'https' depending on whether https is
78 configured within the configs context.
79 """
80 scheme = 'http'
81 if configs and 'https' in configs.complete_contexts():
82 scheme = 'https'
83 return scheme
84
85
86def _get_address_override(endpoint_type=PUBLIC):
87 """Returns any address overrides that the user has defined based on the
88 endpoint type.
89
90 Note: this function allows for the service name to be inserted into the
91 address if the user specifies {service_name}.somehost.org.
92
93 :param endpoint_type: the type of endpoint to retrieve the override
94 value for.
95 :returns: any endpoint address or hostname that the user has overridden
96 or None if an override is not present.
97 """
98 override_key = ADDRESS_MAP[endpoint_type]['override']
99 addr_override = config(override_key)
100 if not addr_override:
101 return None
102 else:
103 return addr_override.format(service_name=service_name())
104
105
69def resolve_address(endpoint_type=PUBLIC):106def resolve_address(endpoint_type=PUBLIC):
70 """Return unit address depending on net config.107 """Return unit address depending on net config.
71108
@@ -77,7 +114,10 @@
77114
78 :param endpoint_type: Network endpoing type115 :param endpoint_type: Network endpoing type
79 """116 """
80 resolved_address = None117 resolved_address = _get_address_override(endpoint_type)
118 if resolved_address:
119 return resolved_address
120
81 vips = config('vip')121 vips = config('vip')
82 if vips:122 if vips:
83 vips = vips.split()123 vips = vips.split()
@@ -109,38 +149,3 @@
109 "clustered=%s)" % (net_type, clustered))149 "clustered=%s)" % (net_type, clustered))
110150
111 return resolved_address151 return resolved_address
112
113
114def endpoint_url(configs, url_template, port, endpoint_type=PUBLIC,
115 override=None):
116 """Returns the correct endpoint URL to advertise to Keystone.
117
118 This method provides the correct endpoint URL which should be advertised to
119 the keystone charm for endpoint creation. This method allows for the url to
120 be overridden to force a keystone endpoint to have specific URL for any of
121 the defined scopes (admin, internal, public).
122
123 :param configs: OSTemplateRenderer config templating object to inspect
124 for a complete https context.
125 :param url_template: str format string for creating the url template. Only
126 two values will be passed - the scheme+hostname
127 returned by the canonical_url and the port.
128 :param endpoint_type: str endpoint type to resolve.
129 :param override: str the name of the config option which overrides the
130 endpoint URL defined by the charm itself. None will
131 disable any overrides (default).
132 """
133 if override:
134 # Return any user-defined overrides for the keystone endpoint URL.
135 user_value = config(override)
136 if user_value:
137 return user_value.strip()
138
139 return url_template % (canonical_url(configs, endpoint_type), port)
140
141
142public_endpoint = partial(endpoint_url, endpoint_type=PUBLIC)
143
144internal_endpoint = partial(endpoint_url, endpoint_type=INTERNAL)
145
146admin_endpoint = partial(endpoint_url, endpoint_type=ADMIN)
147152
=== modified file 'hooks/charmhelpers/contrib/openstack/neutron.py'
--- hooks/charmhelpers/contrib/openstack/neutron.py 2015-04-16 20:24:28 +0000
+++ hooks/charmhelpers/contrib/openstack/neutron.py 2015-06-19 16:15:40 +0000
@@ -172,14 +172,16 @@
172 'services': ['calico-felix',172 'services': ['calico-felix',
173 'bird',173 'bird',
174 'neutron-dhcp-agent',174 'neutron-dhcp-agent',
175 'nova-api-metadata'],175 'nova-api-metadata',
176 'etcd'],
176 'packages': [[headers_package()] + determine_dkms_package(),177 'packages': [[headers_package()] + determine_dkms_package(),
177 ['calico-compute',178 ['calico-compute',
178 'bird',179 'bird',
179 'neutron-dhcp-agent',180 'neutron-dhcp-agent',
180 'nova-api-metadata']],181 'nova-api-metadata',
181 'server_packages': ['neutron-server', 'calico-control'],182 'etcd']],
182 'server_services': ['neutron-server']183 'server_packages': ['neutron-server', 'calico-control', 'etcd'],
184 'server_services': ['neutron-server', 'etcd']
183 },185 },
184 'vsp': {186 'vsp': {
185 'config': '/etc/neutron/plugins/nuage/nuage_plugin.ini',187 'config': '/etc/neutron/plugins/nuage/nuage_plugin.ini',
@@ -256,11 +258,14 @@
256def parse_mappings(mappings):258def parse_mappings(mappings):
257 parsed = {}259 parsed = {}
258 if mappings:260 if mappings:
259 mappings = mappings.split(' ')261 mappings = mappings.split()
260 for m in mappings:262 for m in mappings:
261 p = m.partition(':')263 p = m.partition(':')
262 if p[1] == ':':264 key = p[0].strip()
263 parsed[p[0].strip()] = p[2].strip()265 if p[1]:
266 parsed[key] = p[2].strip()
267 else:
268 parsed[key] = ''
264269
265 return parsed270 return parsed
266271
@@ -283,13 +288,13 @@
283 Returns dict of the form {bridge:port}.288 Returns dict of the form {bridge:port}.
284 """289 """
285 _mappings = parse_mappings(mappings)290 _mappings = parse_mappings(mappings)
286 if not _mappings:291 if not _mappings or list(_mappings.values()) == ['']:
287 if not mappings:292 if not mappings:
288 return {}293 return {}
289294
290 # For backwards-compatibility we need to support port-only provided in295 # For backwards-compatibility we need to support port-only provided in
291 # config.296 # config.
292 _mappings = {default_bridge: mappings.split(' ')[0]}297 _mappings = {default_bridge: mappings.split()[0]}
293298
294 bridges = _mappings.keys()299 bridges = _mappings.keys()
295 ports = _mappings.values()300 ports = _mappings.values()
@@ -309,6 +314,8 @@
309314
310 Mappings must be a space-delimited list of provider:start:end mappings.315 Mappings must be a space-delimited list of provider:start:end mappings.
311316
317 The start:end range is optional and may be omitted.
318
312 Returns dict of the form {provider: (start, end)}.319 Returns dict of the form {provider: (start, end)}.
313 """320 """
314 _mappings = parse_mappings(mappings)321 _mappings = parse_mappings(mappings)
315322
=== modified file 'hooks/charmhelpers/contrib/openstack/utils.py'
--- hooks/charmhelpers/contrib/openstack/utils.py 2015-05-12 14:25:49 +0000
+++ hooks/charmhelpers/contrib/openstack/utils.py 2015-06-19 16:15:40 +0000
@@ -79,6 +79,7 @@
79 ('trusty', 'icehouse'),79 ('trusty', 'icehouse'),
80 ('utopic', 'juno'),80 ('utopic', 'juno'),
81 ('vivid', 'kilo'),81 ('vivid', 'kilo'),
82 ('wily', 'liberty'),
82])83])
8384
8485
@@ -91,6 +92,7 @@
91 ('2014.1', 'icehouse'),92 ('2014.1', 'icehouse'),
92 ('2014.2', 'juno'),93 ('2014.2', 'juno'),
93 ('2015.1', 'kilo'),94 ('2015.1', 'kilo'),
95 ('2015.2', 'liberty'),
94])96])
9597
96# The ugly duckling98# The ugly duckling
@@ -113,6 +115,7 @@
113 ('2.2.0', 'juno'),115 ('2.2.0', 'juno'),
114 ('2.2.1', 'kilo'),116 ('2.2.1', 'kilo'),
115 ('2.2.2', 'kilo'),117 ('2.2.2', 'kilo'),
118 ('2.3.0', 'liberty'),
116])119])
117120
118DEFAULT_LOOPBACK_SIZE = '5G'121DEFAULT_LOOPBACK_SIZE = '5G'
@@ -321,6 +324,9 @@
321 'kilo': 'trusty-updates/kilo',324 'kilo': 'trusty-updates/kilo',
322 'kilo/updates': 'trusty-updates/kilo',325 'kilo/updates': 'trusty-updates/kilo',
323 'kilo/proposed': 'trusty-proposed/kilo',326 'kilo/proposed': 'trusty-proposed/kilo',
327 'liberty': 'trusty-updates/liberty',
328 'liberty/updates': 'trusty-updates/liberty',
329 'liberty/proposed': 'trusty-proposed/liberty',
324 }330 }
325331
326 try:332 try:
@@ -549,6 +555,11 @@
549555
550 pip_create_virtualenv(os.path.join(parent_dir, 'venv'))556 pip_create_virtualenv(os.path.join(parent_dir, 'venv'))
551557
558 # Upgrade setuptools from default virtualenv version. The default version
559 # in trusty breaks update.py in global requirements master branch.
560 pip_install('setuptools', upgrade=True, proxy=http_proxy,
561 venv=os.path.join(parent_dir, 'venv'))
562
552 for p in projects['repositories']:563 for p in projects['repositories']:
553 repo = p['repository']564 repo = p['repository']
554 branch = p['branch']565 branch = p['branch']
@@ -610,24 +621,24 @@
610 else:621 else:
611 repo_dir = dest_dir622 repo_dir = dest_dir
612623
624 venv = os.path.join(parent_dir, 'venv')
625
613 if update_requirements:626 if update_requirements:
614 if not requirements_dir:627 if not requirements_dir:
615 error_out('requirements repo must be cloned before '628 error_out('requirements repo must be cloned before '
616 'updating from global requirements.')629 'updating from global requirements.')
617 _git_update_requirements(repo_dir, requirements_dir)630 _git_update_requirements(venv, repo_dir, requirements_dir)
618631
619 juju_log('Installing git repo from dir: {}'.format(repo_dir))632 juju_log('Installing git repo from dir: {}'.format(repo_dir))
620 if http_proxy:633 if http_proxy:
621 pip_install(repo_dir, proxy=http_proxy,634 pip_install(repo_dir, proxy=http_proxy, venv=venv)
622 venv=os.path.join(parent_dir, 'venv'))
623 else:635 else:
624 pip_install(repo_dir,636 pip_install(repo_dir, venv=venv)
625 venv=os.path.join(parent_dir, 'venv'))
626637
627 return repo_dir638 return repo_dir
628639
629640
630def _git_update_requirements(package_dir, reqs_dir):641def _git_update_requirements(venv, package_dir, reqs_dir):
631 """642 """
632 Update from global requirements.643 Update from global requirements.
633644
@@ -636,12 +647,14 @@
636 """647 """
637 orig_dir = os.getcwd()648 orig_dir = os.getcwd()
638 os.chdir(reqs_dir)649 os.chdir(reqs_dir)
639 cmd = ['python', 'update.py', package_dir]650 python = os.path.join(venv, 'bin/python')
651 cmd = [python, 'update.py', package_dir]
640 try:652 try:
641 subprocess.check_call(cmd)653 subprocess.check_call(cmd)
642 except subprocess.CalledProcessError:654 except subprocess.CalledProcessError:
643 package = os.path.basename(package_dir)655 package = os.path.basename(package_dir)
644 error_out("Error updating {} from global-requirements.txt".format(package))656 error_out("Error updating {} from "
657 "global-requirements.txt".format(package))
645 os.chdir(orig_dir)658 os.chdir(orig_dir)
646659
647660
648661
=== modified file 'hooks/charmhelpers/core/hookenv.py'
--- hooks/charmhelpers/core/hookenv.py 2015-04-16 20:24:28 +0000
+++ hooks/charmhelpers/core/hookenv.py 2015-06-19 16:15:40 +0000
@@ -21,12 +21,14 @@
21# Charm Helpers Developers <juju@lists.ubuntu.com>21# Charm Helpers Developers <juju@lists.ubuntu.com>
2222
23from __future__ import print_function23from __future__ import print_function
24from functools import wraps
24import os25import os
25import json26import json
26import yaml27import yaml
27import subprocess28import subprocess
28import sys29import sys
29import errno30import errno
31import tempfile
30from subprocess import CalledProcessError32from subprocess import CalledProcessError
3133
32import six34import six
@@ -58,15 +60,17 @@
5860
59 will cache the result of unit_get + 'test' for future calls.61 will cache the result of unit_get + 'test' for future calls.
60 """62 """
63 @wraps(func)
61 def wrapper(*args, **kwargs):64 def wrapper(*args, **kwargs):
62 global cache65 global cache
63 key = str((func, args, kwargs))66 key = str((func, args, kwargs))
64 try:67 try:
65 return cache[key]68 return cache[key]
66 except KeyError:69 except KeyError:
67 res = func(*args, **kwargs)70 pass # Drop out of the exception handler scope.
68 cache[key] = res71 res = func(*args, **kwargs)
69 return res72 cache[key] = res
73 return res
70 return wrapper74 return wrapper
7175
7276
@@ -178,7 +182,7 @@
178182
179def remote_unit():183def remote_unit():
180 """The remote unit for the current relation hook"""184 """The remote unit for the current relation hook"""
181 return os.environ['JUJU_REMOTE_UNIT']185 return os.environ.get('JUJU_REMOTE_UNIT', None)
182186
183187
184def service_name():188def service_name():
@@ -250,6 +254,12 @@
250 except KeyError:254 except KeyError:
251 return (self._prev_dict or {})[key]255 return (self._prev_dict or {})[key]
252256
257 def get(self, key, default=None):
258 try:
259 return self[key]
260 except KeyError:
261 return default
262
253 def keys(self):263 def keys(self):
254 prev_keys = []264 prev_keys = []
255 if self._prev_dict is not None:265 if self._prev_dict is not None:
@@ -353,18 +363,49 @@
353 """Set relation information for the current unit"""363 """Set relation information for the current unit"""
354 relation_settings = relation_settings if relation_settings else {}364 relation_settings = relation_settings if relation_settings else {}
355 relation_cmd_line = ['relation-set']365 relation_cmd_line = ['relation-set']
366 accepts_file = "--file" in subprocess.check_output(
367 relation_cmd_line + ["--help"], universal_newlines=True)
356 if relation_id is not None:368 if relation_id is not None:
357 relation_cmd_line.extend(('-r', relation_id))369 relation_cmd_line.extend(('-r', relation_id))
358 for k, v in (list(relation_settings.items()) + list(kwargs.items())):370 settings = relation_settings.copy()
359 if v is None:371 settings.update(kwargs)
360 relation_cmd_line.append('{}='.format(k))372 for key, value in settings.items():
361 else:373 # Force value to be a string: it always should, but some call
362 relation_cmd_line.append('{}={}'.format(k, v))374 # sites pass in things like dicts or numbers.
363 subprocess.check_call(relation_cmd_line)375 if value is not None:
376 settings[key] = "{}".format(value)
377 if accepts_file:
378 # --file was introduced in Juju 1.23.2. Use it by default if
379 # available, since otherwise we'll break if the relation data is
380 # too big. Ideally we should tell relation-set to read the data from
381 # stdin, but that feature is broken in 1.23.2: Bug #1454678.
382 with tempfile.NamedTemporaryFile(delete=False) as settings_file:
383 settings_file.write(yaml.safe_dump(settings).encode("utf-8"))
384 subprocess.check_call(
385 relation_cmd_line + ["--file", settings_file.name])
386 os.remove(settings_file.name)
387 else:
388 for key, value in settings.items():
389 if value is None:
390 relation_cmd_line.append('{}='.format(key))
391 else:
392 relation_cmd_line.append('{}={}'.format(key, value))
393 subprocess.check_call(relation_cmd_line)
364 # Flush cache of any relation-gets for local unit394 # Flush cache of any relation-gets for local unit
365 flush(local_unit())395 flush(local_unit())
366396
367397
398def relation_clear(r_id=None):
399 ''' Clears any relation data already set on relation r_id '''
400 settings = relation_get(rid=r_id,
401 unit=local_unit())
402 for setting in settings:
403 if setting not in ['public-address', 'private-address']:
404 settings[setting] = None
405 relation_set(relation_id=r_id,
406 **settings)
407
408
368@cached409@cached
369def relation_ids(reltype=None):410def relation_ids(reltype=None):
370 """A list of relation_ids"""411 """A list of relation_ids"""
@@ -509,6 +550,11 @@
509 return None550 return None
510551
511552
553def unit_public_ip():
554 """Get this unit's public IP address"""
555 return unit_get('public-address')
556
557
512def unit_private_ip():558def unit_private_ip():
513 """Get this unit's private IP address"""559 """Get this unit's private IP address"""
514 return unit_get('private-address')560 return unit_get('private-address')
@@ -605,3 +651,94 @@
605651
606 The results set by action_set are preserved."""652 The results set by action_set are preserved."""
607 subprocess.check_call(['action-fail', message])653 subprocess.check_call(['action-fail', message])
654
655
656def status_set(workload_state, message):
657 """Set the workload state with a message
658
659 Use status-set to set the workload state with a message which is visible
660 to the user via juju status. If the status-set command is not found then
661 assume this is juju < 1.23 and juju-log the message unstead.
662
663 workload_state -- valid juju workload state.
664 message -- status update message
665 """
666 valid_states = ['maintenance', 'blocked', 'waiting', 'active']
667 if workload_state not in valid_states:
668 raise ValueError(
669 '{!r} is not a valid workload state'.format(workload_state)
670 )
671 cmd = ['status-set', workload_state, message]
672 try:
673 ret = subprocess.call(cmd)
674 if ret == 0:
675 return
676 except OSError as e:
677 if e.errno != errno.ENOENT:
678 raise
679 log_message = 'status-set failed: {} {}'.format(workload_state,
680 message)
681 log(log_message, level='INFO')
682
683
684def status_get():
685 """Retrieve the previously set juju workload state
686
687 If the status-set command is not found then assume this is juju < 1.23 and
688 return 'unknown'
689 """
690 cmd = ['status-get']
691 try:
692 raw_status = subprocess.check_output(cmd, universal_newlines=True)
693 status = raw_status.rstrip()
694 return status
695 except OSError as e:
696 if e.errno == errno.ENOENT:
697 return 'unknown'
698 else:
699 raise
700
701
702def translate_exc(from_exc, to_exc):
703 def inner_translate_exc1(f):
704 def inner_translate_exc2(*args, **kwargs):
705 try:
706 return f(*args, **kwargs)
707 except from_exc:
708 raise to_exc
709
710 return inner_translate_exc2
711
712 return inner_translate_exc1
713
714
715@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
716def is_leader():
717 """Does the current unit hold the juju leadership
718
719 Uses juju to determine whether the current unit is the leader of its peers
720 """
721 cmd = ['is-leader', '--format=json']
722 return json.loads(subprocess.check_output(cmd).decode('UTF-8'))
723
724
725@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
726def leader_get(attribute=None):
727 """Juju leader get value(s)"""
728 cmd = ['leader-get', '--format=json'] + [attribute or '-']
729 return json.loads(subprocess.check_output(cmd).decode('UTF-8'))
730
731
732@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
733def leader_set(settings=None, **kwargs):
734 """Juju leader set value(s)"""
735 log("Juju leader-set '%s'" % (settings), level=DEBUG)
736 cmd = ['leader-set']
737 settings = settings or {}
738 settings.update(kwargs)
739 for k, v in settings.iteritems():
740 if v is None:
741 cmd.append('{}='.format(k))
742 else:
743 cmd.append('{}={}'.format(k, v))
744 subprocess.check_call(cmd)
608745
=== modified file 'hooks/charmhelpers/core/host.py'
--- hooks/charmhelpers/core/host.py 2015-04-16 20:24:28 +0000
+++ hooks/charmhelpers/core/host.py 2015-06-19 16:15:40 +0000
@@ -24,6 +24,7 @@
24import os24import os
25import re25import re
26import pwd26import pwd
27import glob
27import grp28import grp
28import random29import random
29import string30import string
@@ -90,7 +91,7 @@
90 ['service', service_name, 'status'],91 ['service', service_name, 'status'],
91 stderr=subprocess.STDOUT).decode('UTF-8')92 stderr=subprocess.STDOUT).decode('UTF-8')
92 except subprocess.CalledProcessError as e:93 except subprocess.CalledProcessError as e:
93 return 'unrecognized service' not in e.output94 return b'unrecognized service' not in e.output
94 else:95 else:
95 return True96 return True
9697
@@ -269,6 +270,21 @@
269 return None270 return None
270271
271272
273def path_hash(path):
274 """
275 Generate a hash checksum of all files matching 'path'. Standard wildcards
276 like '*' and '?' are supported, see documentation for the 'glob' module for
277 more information.
278
279 :return: dict: A { filename: hash } dictionary for all matched files.
280 Empty if none found.
281 """
282 return {
283 filename: file_hash(filename)
284 for filename in glob.iglob(path)
285 }
286
287
272def check_hash(path, checksum, hash_type='md5'):288def check_hash(path, checksum, hash_type='md5'):
273 """289 """
274 Validate a file using a cryptographic checksum.290 Validate a file using a cryptographic checksum.
@@ -296,23 +312,25 @@
296312
297 @restart_on_change({313 @restart_on_change({
298 '/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ]314 '/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ]
315 '/etc/apache/sites-enabled/*': [ 'apache2' ]
299 })316 })
300 def ceph_client_changed():317 def config_changed():
301 pass # your code here318 pass # your code here
302319
303 In this example, the cinder-api and cinder-volume services320 In this example, the cinder-api and cinder-volume services
304 would be restarted if /etc/ceph/ceph.conf is changed by the321 would be restarted if /etc/ceph/ceph.conf is changed by the
305 ceph_client_changed function.322 ceph_client_changed function. The apache2 service would be
323 restarted if any file matching the pattern got changed, created
324 or removed. Standard wildcards are supported, see documentation
325 for the 'glob' module for more information.
306 """326 """
307 def wrap(f):327 def wrap(f):
308 def wrapped_f(*args, **kwargs):328 def wrapped_f(*args, **kwargs):
309 checksums = {}329 checksums = {path: path_hash(path) for path in restart_map}
310 for path in restart_map:
311 checksums[path] = file_hash(path)
312 f(*args, **kwargs)330 f(*args, **kwargs)
313 restarts = []331 restarts = []
314 for path in restart_map:332 for path in restart_map:
315 if checksums[path] != file_hash(path):333 if path_hash(path) != checksums[path]:
316 restarts += restart_map[path]334 restarts += restart_map[path]
317 services_list = list(OrderedDict.fromkeys(restarts))335 services_list = list(OrderedDict.fromkeys(restarts))
318 if not stopstart:336 if not stopstart:
319337
=== modified file 'hooks/charmhelpers/core/services/base.py'
--- hooks/charmhelpers/core/services/base.py 2015-01-26 09:46:38 +0000
+++ hooks/charmhelpers/core/services/base.py 2015-06-19 16:15:40 +0000
@@ -15,9 +15,9 @@
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1616
17import os17import os
18import re
19import json18import json
20from collections import Iterable19from inspect import getargspec
20from collections import Iterable, OrderedDict
2121
22from charmhelpers.core import host22from charmhelpers.core import host
23from charmhelpers.core import hookenv23from charmhelpers.core import hookenv
@@ -119,7 +119,7 @@
119 """119 """
120 self._ready_file = os.path.join(hookenv.charm_dir(), 'READY-SERVICES.json')120 self._ready_file = os.path.join(hookenv.charm_dir(), 'READY-SERVICES.json')
121 self._ready = None121 self._ready = None
122 self.services = {}122 self.services = OrderedDict()
123 for service in services or []:123 for service in services or []:
124 service_name = service['service']124 service_name = service['service']
125 self.services[service_name] = service125 self.services[service_name] = service
@@ -132,8 +132,8 @@
132 if hook_name == 'stop':132 if hook_name == 'stop':
133 self.stop_services()133 self.stop_services()
134 else:134 else:
135 self.reconfigure_services()
135 self.provide_data()136 self.provide_data()
136 self.reconfigure_services()
137 cfg = hookenv.config()137 cfg = hookenv.config()
138 if cfg.implicit_save:138 if cfg.implicit_save:
139 cfg.save()139 cfg.save()
@@ -145,15 +145,36 @@
145 A provider must have a `name` attribute, which indicates which relation145 A provider must have a `name` attribute, which indicates which relation
146 to set data on, and a `provide_data()` method, which returns a dict of146 to set data on, and a `provide_data()` method, which returns a dict of
147 data to set.147 data to set.
148
149 The `provide_data()` method can optionally accept two parameters:
150
151 * ``remote_service`` The name of the remote service that the data will
152 be provided to. The `provide_data()` method will be called once
153 for each connected service (not unit). This allows the method to
154 tailor its data to the given service.
155 * ``service_ready`` Whether or not the service definition had all of
156 its requirements met, and thus the ``data_ready`` callbacks run.
157
158 Note that the ``provided_data`` methods are now called **after** the
159 ``data_ready`` callbacks are run. This gives the ``data_ready`` callbacks
160 a chance to generate any data necessary for the providing to the remote
161 services.
148 """162 """
149 hook_name = hookenv.hook_name()163 for service_name, service in self.services.items():
150 for service in self.services.values():164 service_ready = self.is_ready(service_name)
151 for provider in service.get('provided_data', []):165 for provider in service.get('provided_data', []):
152 if re.match(r'{}-relation-(joined|changed)'.format(provider.name), hook_name):166 for relid in hookenv.relation_ids(provider.name):
153 data = provider.provide_data()167 units = hookenv.related_units(relid)
154 _ready = provider._is_ready(data) if hasattr(provider, '_is_ready') else data168 if not units:
155 if _ready:169 continue
156 hookenv.relation_set(None, data)170 remote_service = units[0].split('/')[0]
171 argspec = getargspec(provider.provide_data)
172 if len(argspec.args) > 1:
173 data = provider.provide_data(remote_service, service_ready)
174 else:
175 data = provider.provide_data()
176 if data:
177 hookenv.relation_set(relid, data)
157178
158 def reconfigure_services(self, *service_names):179 def reconfigure_services(self, *service_names):
159 """180 """
160181
=== modified file 'hooks/charmhelpers/fetch/__init__.py'
--- hooks/charmhelpers/fetch/__init__.py 2015-05-01 14:57:35 +0000
+++ hooks/charmhelpers/fetch/__init__.py 2015-06-19 16:15:40 +0000
@@ -158,7 +158,7 @@
158158
159def apt_cache(in_memory=True):159def apt_cache(in_memory=True):
160 """Build and return an apt cache"""160 """Build and return an apt cache"""
161 import apt_pkg161 from apt import apt_pkg
162 apt_pkg.init()162 apt_pkg.init()
163 if in_memory:163 if in_memory:
164 apt_pkg.config.set("Dir::Cache::pkgcache", "")164 apt_pkg.config.set("Dir::Cache::pkgcache", "")
165165
=== modified file 'tests/charmhelpers/contrib/amulet/utils.py'
--- tests/charmhelpers/contrib/amulet/utils.py 2015-05-05 20:28:18 +0000
+++ tests/charmhelpers/contrib/amulet/utils.py 2015-06-19 16:15:40 +0000
@@ -15,13 +15,15 @@
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1616
17import ConfigParser17import ConfigParser
18import distro_info
18import io19import io
19import logging20import logging
21import os
20import re22import re
23import six
21import sys24import sys
22import time25import time
2326import urlparse
24import six
2527
2628
27class AmuletUtils(object):29class AmuletUtils(object):
@@ -33,6 +35,7 @@
3335
34 def __init__(self, log_level=logging.ERROR):36 def __init__(self, log_level=logging.ERROR):
35 self.log = self.get_logger(level=log_level)37 self.log = self.get_logger(level=log_level)
38 self.ubuntu_releases = self.get_ubuntu_releases()
3639
37 def get_logger(self, name="amulet-logger", level=logging.DEBUG):40 def get_logger(self, name="amulet-logger", level=logging.DEBUG):
38 """Get a logger object that will log to stdout."""41 """Get a logger object that will log to stdout."""
@@ -70,12 +73,44 @@
70 else:73 else:
71 return False74 return False
7275
76 def get_ubuntu_release_from_sentry(self, sentry_unit):
77 """Get Ubuntu release codename from sentry unit.
78
79 :param sentry_unit: amulet sentry/service unit pointer
80 :returns: list of strings - release codename, failure message
81 """
82 msg = None
83 cmd = 'lsb_release -cs'
84 release, code = sentry_unit.run(cmd)
85 if code == 0:
86 self.log.debug('{} lsb_release: {}'.format(
87 sentry_unit.info['unit_name'], release))
88 else:
89 msg = ('{} `{}` returned {} '
90 '{}'.format(sentry_unit.info['unit_name'],
91 cmd, release, code))
92 if release not in self.ubuntu_releases:
93 msg = ("Release ({}) not found in Ubuntu releases "
94 "({})".format(release, self.ubuntu_releases))
95 return release, msg
96
73 def validate_services(self, commands):97 def validate_services(self, commands):
74 """Validate services.98 """Validate that lists of commands succeed on service units. Can be
7599 used to verify system services are running on the corresponding
76 Verify the specified services are running on the corresponding
77 service units.100 service units.
78 """101
102 :param commands: dict with sentry keys and arbitrary command list vals
103 :returns: None if successful, Failure string message otherwise
104 """
105 self.log.debug('Checking status of system services...')
106
107 # /!\ DEPRECATION WARNING (beisner):
108 # New and existing tests should be rewritten to use
109 # validate_services_by_name() as it is aware of init systems.
110 self.log.warn('/!\\ DEPRECATION WARNING: use '
111 'validate_services_by_name instead of validate_services '
112 'due to init system differences.')
113
79 for k, v in six.iteritems(commands):114 for k, v in six.iteritems(commands):
80 for cmd in v:115 for cmd in v:
81 output, code = k.run(cmd)116 output, code = k.run(cmd)
@@ -86,6 +121,41 @@
86 return "command `{}` returned {}".format(cmd, str(code))121 return "command `{}` returned {}".format(cmd, str(code))
87 return None122 return None
88123
124 def validate_services_by_name(self, sentry_services):
125 """Validate system service status by service name, automatically
126 detecting init system based on Ubuntu release codename.
127
128 :param sentry_services: dict with sentry keys and svc list values
129 :returns: None if successful, Failure string message otherwise
130 """
131 self.log.debug('Checking status of system services...')
132
133 # Point at which systemd became a thing
134 systemd_switch = self.ubuntu_releases.index('vivid')
135
136 for sentry_unit, services_list in six.iteritems(sentry_services):
137 # Get lsb_release codename from unit
138 release, ret = self.get_ubuntu_release_from_sentry(sentry_unit)
139 if ret:
140 return ret
141
142 for service_name in services_list:
143 if (self.ubuntu_releases.index(release) >= systemd_switch or
144 service_name == "rabbitmq-server"):
145 # init is systemd
146 cmd = 'sudo service {} status'.format(service_name)
147 elif self.ubuntu_releases.index(release) < systemd_switch:
148 # init is upstart
149 cmd = 'sudo status {}'.format(service_name)
150
151 output, code = sentry_unit.run(cmd)
152 self.log.debug('{} `{}` returned '
153 '{}'.format(sentry_unit.info['unit_name'],
154 cmd, code))
155 if code != 0:
156 return "command `{}` returned {}".format(cmd, str(code))
157 return None
158
89 def _get_config(self, unit, filename):159 def _get_config(self, unit, filename):
90 """Get a ConfigParser object for parsing a unit's config file."""160 """Get a ConfigParser object for parsing a unit's config file."""
91 file_contents = unit.file_contents(filename)161 file_contents = unit.file_contents(filename)
@@ -104,6 +174,9 @@
104 Verify that the specified section of the config file contains174 Verify that the specified section of the config file contains
105 the expected option key:value pairs.175 the expected option key:value pairs.
106 """176 """
177 self.log.debug('Validating config file data ({} in {} on {})'
178 '...'.format(section, config_file,
179 sentry_unit.info['unit_name']))
107 config = self._get_config(sentry_unit, config_file)180 config = self._get_config(sentry_unit, config_file)
108181
109 if section != 'DEFAULT' and not config.has_section(section):182 if section != 'DEFAULT' and not config.has_section(section):
@@ -321,3 +394,15 @@
321394
322 def endpoint_error(self, name, data):395 def endpoint_error(self, name, data):
323 return 'unexpected endpoint data in {} - {}'.format(name, data)396 return 'unexpected endpoint data in {} - {}'.format(name, data)
397
398 def get_ubuntu_releases(self):
399 """Return a list of all Ubuntu releases in order of release."""
400 _d = distro_info.UbuntuDistroInfo()
401 _release_list = _d.all
402 self.log.debug('Ubuntu release list: {}'.format(_release_list))
403 return _release_list
404
405 def file_to_url(self, file_rel_path):
406 """Convert a relative file path to a file URL."""
407 _abs_path = os.path.abspath(file_rel_path)
408 return urlparse.urlparse(_abs_path, scheme='file').geturl()
324409
=== modified file 'tests/charmhelpers/contrib/openstack/amulet/deployment.py'
--- tests/charmhelpers/contrib/openstack/amulet/deployment.py 2015-05-05 20:28:18 +0000
+++ tests/charmhelpers/contrib/openstack/amulet/deployment.py 2015-06-19 16:15:40 +0000
@@ -110,7 +110,8 @@
110 (self.precise_essex, self.precise_folsom, self.precise_grizzly,110 (self.precise_essex, self.precise_folsom, self.precise_grizzly,
111 self.precise_havana, self.precise_icehouse,111 self.precise_havana, self.precise_icehouse,
112 self.trusty_icehouse, self.trusty_juno, self.utopic_juno,112 self.trusty_icehouse, self.trusty_juno, self.utopic_juno,
113 self.trusty_kilo, self.vivid_kilo) = range(10)113 self.trusty_kilo, self.vivid_kilo, self.trusty_liberty,
114 self.wily_liberty) = range(12)
114115
115 releases = {116 releases = {
116 ('precise', None): self.precise_essex,117 ('precise', None): self.precise_essex,
@@ -121,8 +122,10 @@
121 ('trusty', None): self.trusty_icehouse,122 ('trusty', None): self.trusty_icehouse,
122 ('trusty', 'cloud:trusty-juno'): self.trusty_juno,123 ('trusty', 'cloud:trusty-juno'): self.trusty_juno,
123 ('trusty', 'cloud:trusty-kilo'): self.trusty_kilo,124 ('trusty', 'cloud:trusty-kilo'): self.trusty_kilo,
125 ('trusty', 'cloud:trusty-liberty'): self.trusty_liberty,
124 ('utopic', None): self.utopic_juno,126 ('utopic', None): self.utopic_juno,
125 ('vivid', None): self.vivid_kilo}127 ('vivid', None): self.vivid_kilo,
128 ('wily', None): self.wily_liberty}
126 return releases[(self.series, self.openstack)]129 return releases[(self.series, self.openstack)]
127130
128 def _get_openstack_release_string(self):131 def _get_openstack_release_string(self):
@@ -138,6 +141,7 @@
138 ('trusty', 'icehouse'),141 ('trusty', 'icehouse'),
139 ('utopic', 'juno'),142 ('utopic', 'juno'),
140 ('vivid', 'kilo'),143 ('vivid', 'kilo'),
144 ('wily', 'liberty'),
141 ])145 ])
142 if self.openstack:146 if self.openstack:
143 os_origin = self.openstack.split(':')[1]147 os_origin = self.openstack.split(':')[1]
144148
=== modified file 'tests/charmhelpers/contrib/openstack/amulet/utils.py'
--- tests/charmhelpers/contrib/openstack/amulet/utils.py 2015-02-10 18:50:39 +0000
+++ tests/charmhelpers/contrib/openstack/amulet/utils.py 2015-06-19 16:15:40 +0000
@@ -16,15 +16,15 @@
1616
17import logging17import logging
18import os18import os
19import six
19import time20import time
20import urllib21import urllib
2122
22import glanceclient.v1.client as glance_client23import glanceclient.v1.client as glance_client
24import heatclient.v1.client as heat_client
23import keystoneclient.v2_0 as keystone_client25import keystoneclient.v2_0 as keystone_client
24import novaclient.v1_1.client as nova_client26import novaclient.v1_1.client as nova_client
2527
26import six
27
28from charmhelpers.contrib.amulet.utils import (28from charmhelpers.contrib.amulet.utils import (
29 AmuletUtils29 AmuletUtils
30)30)
@@ -37,7 +37,7 @@
37 """OpenStack amulet utilities.37 """OpenStack amulet utilities.
3838
39 This class inherits from AmuletUtils and has additional support39 This class inherits from AmuletUtils and has additional support
40 that is specifically for use by OpenStack charms.40 that is specifically for use by OpenStack charm tests.
41 """41 """
4242
43 def __init__(self, log_level=ERROR):43 def __init__(self, log_level=ERROR):
@@ -51,6 +51,8 @@
51 Validate actual endpoint data vs expected endpoint data. The ports51 Validate actual endpoint data vs expected endpoint data. The ports
52 are used to find the matching endpoint.52 are used to find the matching endpoint.
53 """53 """
54 self.log.debug('Validating endpoint data...')
55 self.log.debug('actual: {}'.format(repr(endpoints)))
54 found = False56 found = False
55 for ep in endpoints:57 for ep in endpoints:
56 self.log.debug('endpoint: {}'.format(repr(ep)))58 self.log.debug('endpoint: {}'.format(repr(ep)))
@@ -77,6 +79,7 @@
77 Validate a list of actual service catalog endpoints vs a list of79 Validate a list of actual service catalog endpoints vs a list of
78 expected service catalog endpoints.80 expected service catalog endpoints.
79 """81 """
82 self.log.debug('Validating service catalog endpoint data...')
80 self.log.debug('actual: {}'.format(repr(actual)))83 self.log.debug('actual: {}'.format(repr(actual)))
81 for k, v in six.iteritems(expected):84 for k, v in six.iteritems(expected):
82 if k in actual:85 if k in actual:
@@ -93,6 +96,7 @@
93 Validate a list of actual tenant data vs list of expected tenant96 Validate a list of actual tenant data vs list of expected tenant
94 data.97 data.
95 """98 """
99 self.log.debug('Validating tenant data...')
96 self.log.debug('actual: {}'.format(repr(actual)))100 self.log.debug('actual: {}'.format(repr(actual)))
97 for e in expected:101 for e in expected:
98 found = False102 found = False
@@ -114,6 +118,7 @@
114 Validate a list of actual role data vs a list of expected role118 Validate a list of actual role data vs a list of expected role
115 data.119 data.
116 """120 """
121 self.log.debug('Validating role data...')
117 self.log.debug('actual: {}'.format(repr(actual)))122 self.log.debug('actual: {}'.format(repr(actual)))
118 for e in expected:123 for e in expected:
119 found = False124 found = False
@@ -134,6 +139,7 @@
134 Validate a list of actual user data vs a list of expected user139 Validate a list of actual user data vs a list of expected user
135 data.140 data.
136 """141 """
142 self.log.debug('Validating user data...')
137 self.log.debug('actual: {}'.format(repr(actual)))143 self.log.debug('actual: {}'.format(repr(actual)))
138 for e in expected:144 for e in expected:
139 found = False145 found = False
@@ -155,17 +161,20 @@
155161
156 Validate a list of actual flavors vs a list of expected flavors.162 Validate a list of actual flavors vs a list of expected flavors.
157 """163 """
164 self.log.debug('Validating flavor data...')
158 self.log.debug('actual: {}'.format(repr(actual)))165 self.log.debug('actual: {}'.format(repr(actual)))
159 act = [a.name for a in actual]166 act = [a.name for a in actual]
160 return self._validate_list_data(expected, act)167 return self._validate_list_data(expected, act)
161168
162 def tenant_exists(self, keystone, tenant):169 def tenant_exists(self, keystone, tenant):
163 """Return True if tenant exists."""170 """Return True if tenant exists."""
171 self.log.debug('Checking if tenant exists ({})...'.format(tenant))
164 return tenant in [t.name for t in keystone.tenants.list()]172 return tenant in [t.name for t in keystone.tenants.list()]
165173
166 def authenticate_keystone_admin(self, keystone_sentry, user, password,174 def authenticate_keystone_admin(self, keystone_sentry, user, password,
167 tenant):175 tenant):
168 """Authenticates admin user with the keystone admin endpoint."""176 """Authenticates admin user with the keystone admin endpoint."""
177 self.log.debug('Authenticating keystone admin...')
169 unit = keystone_sentry178 unit = keystone_sentry
170 service_ip = unit.relation('shared-db',179 service_ip = unit.relation('shared-db',
171 'mysql:shared-db')['private-address']180 'mysql:shared-db')['private-address']
@@ -175,6 +184,7 @@
175184
176 def authenticate_keystone_user(self, keystone, user, password, tenant):185 def authenticate_keystone_user(self, keystone, user, password, tenant):
177 """Authenticates a regular user with the keystone public endpoint."""186 """Authenticates a regular user with the keystone public endpoint."""
187 self.log.debug('Authenticating keystone user ({})...'.format(user))
178 ep = keystone.service_catalog.url_for(service_type='identity',188 ep = keystone.service_catalog.url_for(service_type='identity',
179 endpoint_type='publicURL')189 endpoint_type='publicURL')
180 return keystone_client.Client(username=user, password=password,190 return keystone_client.Client(username=user, password=password,
@@ -182,12 +192,21 @@
182192
183 def authenticate_glance_admin(self, keystone):193 def authenticate_glance_admin(self, keystone):
184 """Authenticates admin user with glance."""194 """Authenticates admin user with glance."""
195 self.log.debug('Authenticating glance admin...')
185 ep = keystone.service_catalog.url_for(service_type='image',196 ep = keystone.service_catalog.url_for(service_type='image',
186 endpoint_type='adminURL')197 endpoint_type='adminURL')
187 return glance_client.Client(ep, token=keystone.auth_token)198 return glance_client.Client(ep, token=keystone.auth_token)
188199
200 def authenticate_heat_admin(self, keystone):
201 """Authenticates the admin user with heat."""
202 self.log.debug('Authenticating heat admin...')
203 ep = keystone.service_catalog.url_for(service_type='orchestration',
204 endpoint_type='publicURL')
205 return heat_client.Client(endpoint=ep, token=keystone.auth_token)
206
189 def authenticate_nova_user(self, keystone, user, password, tenant):207 def authenticate_nova_user(self, keystone, user, password, tenant):
190 """Authenticates a regular user with nova-api."""208 """Authenticates a regular user with nova-api."""
209 self.log.debug('Authenticating nova user ({})...'.format(user))
191 ep = keystone.service_catalog.url_for(service_type='identity',210 ep = keystone.service_catalog.url_for(service_type='identity',
192 endpoint_type='publicURL')211 endpoint_type='publicURL')
193 return nova_client.Client(username=user, api_key=password,212 return nova_client.Client(username=user, api_key=password,
@@ -195,6 +214,7 @@
195214
196 def create_cirros_image(self, glance, image_name):215 def create_cirros_image(self, glance, image_name):
197 """Download the latest cirros image and upload it to glance."""216 """Download the latest cirros image and upload it to glance."""
217 self.log.debug('Creating glance image ({})...'.format(image_name))
198 http_proxy = os.getenv('AMULET_HTTP_PROXY')218 http_proxy = os.getenv('AMULET_HTTP_PROXY')
199 self.log.debug('AMULET_HTTP_PROXY: {}'.format(http_proxy))219 self.log.debug('AMULET_HTTP_PROXY: {}'.format(http_proxy))
200 if http_proxy:220 if http_proxy:
@@ -235,6 +255,11 @@
235255
236 def delete_image(self, glance, image):256 def delete_image(self, glance, image):
237 """Delete the specified image."""257 """Delete the specified image."""
258
259 # /!\ DEPRECATION WARNING
260 self.log.warn('/!\\ DEPRECATION WARNING: use '
261 'delete_resource instead of delete_image.')
262 self.log.debug('Deleting glance image ({})...'.format(image))
238 num_before = len(list(glance.images.list()))263 num_before = len(list(glance.images.list()))
239 glance.images.delete(image)264 glance.images.delete(image)
240265
@@ -254,6 +279,8 @@
254279
255 def create_instance(self, nova, image_name, instance_name, flavor):280 def create_instance(self, nova, image_name, instance_name, flavor):
256 """Create the specified instance."""281 """Create the specified instance."""
282 self.log.debug('Creating instance '
283 '({}|{}|{})'.format(instance_name, image_name, flavor))
257 image = nova.images.find(name=image_name)284 image = nova.images.find(name=image_name)
258 flavor = nova.flavors.find(name=flavor)285 flavor = nova.flavors.find(name=flavor)
259 instance = nova.servers.create(name=instance_name, image=image,286 instance = nova.servers.create(name=instance_name, image=image,
@@ -276,6 +303,11 @@
276303
277 def delete_instance(self, nova, instance):304 def delete_instance(self, nova, instance):
278 """Delete the specified instance."""305 """Delete the specified instance."""
306
307 # /!\ DEPRECATION WARNING
308 self.log.warn('/!\\ DEPRECATION WARNING: use '
309 'delete_resource instead of delete_instance.')
310 self.log.debug('Deleting instance ({})...'.format(instance))
279 num_before = len(list(nova.servers.list()))311 num_before = len(list(nova.servers.list()))
280 nova.servers.delete(instance)312 nova.servers.delete(instance)
281313
@@ -292,3 +324,90 @@
292 return False324 return False
293325
294 return True326 return True
327
328 def create_or_get_keypair(self, nova, keypair_name="testkey"):
329 """Create a new keypair, or return pointer if it already exists."""
330 try:
331 _keypair = nova.keypairs.get(keypair_name)
332 self.log.debug('Keypair ({}) already exists, '
333 'using it.'.format(keypair_name))
334 return _keypair
335 except:
336 self.log.debug('Keypair ({}) does not exist, '
337 'creating it.'.format(keypair_name))
338
339 _keypair = nova.keypairs.create(name=keypair_name)
340 return _keypair
341
342 def delete_resource(self, resource, resource_id,
343 msg="resource", max_wait=120):
344 """Delete one openstack resource, such as one instance, keypair,
345 image, volume, stack, etc., and confirm deletion within max wait time.
346
347 :param resource: pointer to os resource type, ex:glance_client.images
348 :param resource_id: unique name or id for the openstack resource
349 :param msg: text to identify purpose in logging
350 :param max_wait: maximum wait time in seconds
351 :returns: True if successful, otherwise False
352 """
353 num_before = len(list(resource.list()))
354 resource.delete(resource_id)
355
356 tries = 0
357 num_after = len(list(resource.list()))
358 while num_after != (num_before - 1) and tries < (max_wait / 4):
359 self.log.debug('{} delete check: '
360 '{} [{}:{}] {}'.format(msg, tries,
361 num_before,
362 num_after,
363 resource_id))
364 time.sleep(4)
365 num_after = len(list(resource.list()))
366 tries += 1
367
368 self.log.debug('{}: expected, actual count = {}, '
369 '{}'.format(msg, num_before - 1, num_after))
370
371 if num_after == (num_before - 1):
372 return True
373 else:
374 self.log.error('{} delete timed out'.format(msg))
375 return False
376
377 def resource_reaches_status(self, resource, resource_id,
378 expected_stat='available',
379 msg='resource', max_wait=120):
380 """Wait for an openstack resources status to reach an
381 expected status within a specified time. Useful to confirm that
382 nova instances, cinder vols, snapshots, glance images, heat stacks
383 and other resources eventually reach the expected status.
384
385 :param resource: pointer to os resource type, ex: heat_client.stacks
386 :param resource_id: unique id for the openstack resource
387 :param expected_stat: status to expect resource to reach
388 :param msg: text to identify purpose in logging
389 :param max_wait: maximum wait time in seconds
390 :returns: True if successful, False if status is not reached
391 """
392
393 tries = 0
394 resource_stat = resource.get(resource_id).status
395 while resource_stat != expected_stat and tries < (max_wait / 4):
396 self.log.debug('{} status check: '
397 '{} [{}:{}] {}'.format(msg, tries,
398 resource_stat,
399 expected_stat,
400 resource_id))
401 time.sleep(4)
402 resource_stat = resource.get(resource_id).status
403 tries += 1
404
405 self.log.debug('{}: expected, actual status = {}, '
406 '{}'.format(msg, resource_stat, expected_stat))
407
408 if resource_stat == expected_stat:
409 return True
410 else:
411 self.log.debug('{} never reached expected status: '
412 '{}'.format(resource_id, expected_stat))
413 return False
295414
=== modified file 'unit_tests/test_horizon_hooks.py'
--- unit_tests/test_horizon_hooks.py 2015-04-07 13:58:41 +0000
+++ unit_tests/test_horizon_hooks.py 2015-06-19 16:15:40 +0000
@@ -129,7 +129,7 @@
129 self.apt_install.assert_called_with(['foo', 'bar'], fatal=True)129 self.apt_install.assert_called_with(['foo', 'bar'], fatal=True)
130 self.git_install.assert_called_with(projects_yaml)130 self.git_install.assert_called_with(projects_yaml)
131131
132 @patch('charmhelpers.core.host.file_hash')132 @patch('charmhelpers.core.host.path_hash')
133 @patch('charmhelpers.core.host.service')133 @patch('charmhelpers.core.host.service')
134 @patch.object(utils, 'git_install_requested')134 @patch.object(utils, 'git_install_requested')
135 def test_upgrade_charm_hook(self, _git_requested, _service, _hash):135 def test_upgrade_charm_hook(self, _git_requested, _service, _hash):

Subscribers

People subscribed via source and target branches