Merge lp:~1chb1n/charms/trusty/glance/next-amulet-update into lp:~openstack-charmers-archive/charms/trusty/glance/trunk

Proposed by Ryan Beisner
Status: Superseded
Proposed branch: lp:~1chb1n/charms/trusty/glance/next-amulet-update
Merge into: lp:~openstack-charmers-archive/charms/trusty/glance/trunk
Diff against target: 3606 lines (+1746/-542) (has conflicts)
28 files modified
Makefile (+10/-15)
README.md (+77/-0)
hooks/charmhelpers/contrib/hahelpers/cluster.py (+46/-2)
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/neutron.py (+16/-9)
hooks/charmhelpers/contrib/openstack/utils.py (+82/-22)
hooks/charmhelpers/contrib/python/packages.py (+30/-5)
hooks/charmhelpers/core/hookenv.py (+231/-38)
hooks/charmhelpers/core/host.py (+25/-7)
hooks/charmhelpers/core/services/base.py (+43/-19)
hooks/charmhelpers/fetch/__init__.py (+1/-1)
hooks/charmhelpers/fetch/giturl.py (+7/-5)
hooks/glance_relations.py (+3/-3)
hooks/glance_utils.py (+50/-4)
metadata.yaml (+1/-1)
tests/00-setup (+5/-1)
tests/020-basic-trusty-liberty (+11/-0)
tests/021-basic-wily-liberty (+9/-0)
tests/README (+9/-0)
tests/basic_deployment.py (+288/-326)
tests/charmhelpers/contrib/amulet/utils.py (+228/-10)
tests/charmhelpers/contrib/openstack/amulet/deployment.py (+41/-5)
tests/charmhelpers/contrib/openstack/amulet/utils.py (+358/-51)
tests/tests.yaml (+18/-0)
unit_tests/test_glance_relations.py (+11/-5)
unit_tests/test_glance_utils.py (+17/-7)
Text conflict in README.md
Text conflict in hooks/charmhelpers/contrib/hahelpers/cluster.py
Text conflict in tests/basic_deployment.py
To merge this branch: bzr merge lp:~1chb1n/charms/trusty/glance/next-amulet-update
Reviewer Review Type Date Requested Status
Corey Bryant Pending
Review via email: mp+263411@code.launchpad.net

This proposal has been superseded by a proposal from 2015-06-30.

Description of the change

Update amulet tests for Kilo, prep for wily. Sync hooks/charmhelpers; Sync tests/charmhelpers.

To post a comment you must log in.
124. By Ryan Beisner

update tests

125. By Ryan Beisner

update tags for consistency with other openstack charms

126. By Ryan Beisner

update tests for vivid-kilo

Unmerged revisions

126. By Ryan Beisner

update tests for vivid-kilo

125. By Ryan Beisner

update tags for consistency with other openstack charms

124. By Ryan Beisner

update tests

123. By Ryan Beisner

sync tests/charmhelpers

122. By Ryan Beisner

sync hooks/charmhelpers

121. By Liam Young

[corey.bryant, r=gnuoy] charmhelper sync

120. By Billy Olsen

[corey.bryant,r=billy-olsen] Fix global requirements for git-deploy.

119. By Corey Bryant

[billy-olsen,r=corey.bryant] Provide support for user-specified public endpoint hostname.

118. By James Page

Add support for leader-election

117. By James Page

Fixup glance-api template sections for Kilo release.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'Makefile'
--- Makefile 2015-04-16 21:32:02 +0000
+++ Makefile 2015-06-30 20:18:04 +0000
@@ -2,16 +2,18 @@
2PYTHON := /usr/bin/env python2PYTHON := /usr/bin/env python
33
4lint:4lint:
5 @echo "Running flake8 tests: "5 @flake8 --exclude hooks/charmhelpers,tests/charmhelpers \
6 @flake8 --exclude hooks/charmhelpers actions hooks unit_tests tests6 actions hooks unit_tests tests
7 @echo "OK"
8 @echo "Running charm proof: "
9 @charm proof7 @charm proof
10 @echo "OK"
118
12unit_test:9test:
10 @# Bundletester expects unit tests here.
13 @$(PYTHON) /usr/bin/nosetests --nologcapture --with-coverage unit_tests11 @$(PYTHON) /usr/bin/nosetests --nologcapture --with-coverage unit_tests
1412
13functional_test:
14 @echo Starting Amulet tests...
15 @juju test -v -p AMULET_HTTP_PROXY,AMULET_OS_VIP --timeout 2700
16
15bin/charm_helpers_sync.py:17bin/charm_helpers_sync.py:
16 @mkdir -p bin18 @mkdir -p bin
17 @bzr cat lp:charm-helpers/tools/charm_helpers_sync/charm_helpers_sync.py \19 @bzr cat lp:charm-helpers/tools/charm_helpers_sync/charm_helpers_sync.py \
@@ -21,15 +23,8 @@
21 @$(PYTHON) bin/charm_helpers_sync.py -c charm-helpers-hooks.yaml23 @$(PYTHON) bin/charm_helpers_sync.py -c charm-helpers-hooks.yaml
22 @$(PYTHON) bin/charm_helpers_sync.py -c charm-helpers-tests.yaml24 @$(PYTHON) bin/charm_helpers_sync.py -c charm-helpers-tests.yaml
2325
24test:26publish: lint test
25 @echo Starting Amulet tests...
26 # /!\ Note: The -v should only be temporary until Amulet sends
27 # raise_status() messages to stderr:
28 # https://bugs.launchpad.net/amulet/+bug/1320357
29 @juju test -v -p AMULET_HTTP_PROXY,AMULET_OS_VIP --timeout 2700
30
31publish: lint unit_test
32 bzr push lp:charms/glance27 bzr push lp:charms/glance
33 bzr push lp:charms/trusty/glance28 bzr push lp:charms/trusty/glance
3429
35all: unit_test lint30all: test lint
3631
=== modified file 'README.md'
--- README.md 2015-04-30 15:23:58 +0000
+++ README.md 2015-06-30 20:18:04 +0000
@@ -86,6 +86,7 @@
8686
87The minimum openstack-origin-git config required to deploy from source is:87The minimum openstack-origin-git config required to deploy from source is:
8888
89<<<<<<< TREE
89 openstack-origin-git: include-file://glance-juno.yaml90 openstack-origin-git: include-file://glance-juno.yaml
9091
91 glance-juno.yaml92 glance-juno.yaml
@@ -97,6 +98,18 @@
97 - {name: glance,98 - {name: glance,
98 repository: 'git://github.com/openstack/glance',99 repository: 'git://github.com/openstack/glance',
99 branch: stable/juno}100 branch: stable/juno}
101=======
102 openstack-origin-git: include-file://glance-juno.yaml
103
104 glance-juno.yaml
105 repositories:
106 - {name: requirements,
107 repository: 'git://github.com/openstack/requirements',
108 branch: stable/juno}
109 - {name: glance,
110 repository: 'git://github.com/openstack/glance',
111 branch: stable/juno}
112>>>>>>> MERGE-SOURCE
100113
101Note that there are only two 'name' values the charm knows about: 'requirements'114Note that there are only two 'name' values the charm knows about: 'requirements'
102and 'glance'. These repositories must correspond to these 'name' values.115and 'glance'. These repositories must correspond to these 'name' values.
@@ -106,6 +119,7 @@
106119
107The following is a full list of current tip repos (may not be up-to-date):120The following is a full list of current tip repos (may not be up-to-date):
108121
122<<<<<<< TREE
109 openstack-origin-git: include-file://glance-master.yaml123 openstack-origin-git: include-file://glance-master.yaml
110124
111 glance-master.yaml125 glance-master.yaml
@@ -168,6 +182,69 @@
168 - {name: glance,182 - {name: glance,
169 repository: 'git://github.com/openstack/glance',183 repository: 'git://github.com/openstack/glance',
170 branch: master}184 branch: master}
185=======
186 openstack-origin-git: include-file://glance-master.yaml
187
188 glance-master.yaml
189 repositories:
190 - {name: requirements,
191 repository: 'git://github.com/openstack/requirements',
192 branch: master}
193 - {name: oslo-concurrency,
194 repository: 'git://github.com/openstack/oslo.concurrency',
195 branch: master}
196 - {name: oslo-config,
197 repository: 'git://github.com/openstack/oslo.config',
198 branch: master}
199 - {name: oslo-db,
200 repository: 'git://github.com/openstack/oslo.db',
201 branch: master}
202 - {name: oslo-i18n,
203 repository: 'git://github.com/openstack/oslo.i18n',
204 branch: master}
205 - {name: oslo-messaging,
206 repository: 'git://github.com/openstack/oslo.messaging',
207 branch: master}
208 - {name: oslo-serialization,
209 repository: 'git://github.com/openstack/oslo.serialization',
210 branch: master}
211 - {name: oslo-utils,
212 repository: 'git://github.com/openstack/oslo.utils',
213 branch: master}
214 - {name: oslo-vmware,
215 repository: 'git://github.com/openstack/oslo.vmware',
216 branch: master}
217 - {name: osprofiler,
218 repository: 'git://github.com/stackforge/osprofiler',
219 branch: master}
220 - {name: pbr,
221 repository: 'git://github.com/openstack-dev/pbr',
222 branch: master}
223 - {name: python-keystoneclient,
224 repository: 'git://github.com/openstack/python-keystoneclient',
225 branch: master}
226 - {name: python-swiftclient,
227 repository: 'git://github.com/openstack/python-swiftclient',
228 branch: master}
229 - {name: sqlalchemy-migrate,
230 repository: 'git://github.com/stackforge/sqlalchemy-migrate',
231 branch: master}
232 - {name: stevedore,
233 repository: 'git://github.com/openstack/stevedore',
234 branch: master}
235 - {name: wsme,
236 repository: 'git://github.com/stackforge/wsme',
237 branch: master}
238 - {name: keystonemiddleware,
239 repository: 'git://github.com/openstack/keystonemiddleware',
240 branch: master}
241 - {name: glance-store,
242 repository: 'git://github.com/openstack/glance_store',
243 branch: master}
244 - {name: glance,
245 repository: 'git://github.com/openstack/glance',
246 branch: master}
247>>>>>>> MERGE-SOURCE
171248
172Contact Information249Contact Information
173-------------------250-------------------
174251
=== modified file 'hooks/charmhelpers/contrib/hahelpers/cluster.py'
--- hooks/charmhelpers/contrib/hahelpers/cluster.py 2015-06-18 23:26:31 +0000
+++ hooks/charmhelpers/contrib/hahelpers/cluster.py 2015-06-30 20:18:04 +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,
@@ -63,17 +64,30 @@
63 pass64 pass
6465
6566
67class CRMDCNotFound(Exception):
68 pass
69
70
66def is_elected_leader(resource):71def is_elected_leader(resource):
67 """72 """
68 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.
6974
70 It relies on two mechanisms to determine leadership:75 It relies on two mechanisms to determine leadership:
71 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
72 determine leadership.79 determine leadership.
73 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
74 determined as being "the alive unit with the lowest unit numer". In81 determined as being "the alive unit with the lowest unit numer". In
75 other words, the oldest surviving unit.82 other words, the oldest surviving unit.
76 """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
77 if is_clustered():91 if is_clustered():
78 if not is_crm_leader(resource):92 if not is_crm_leader(resource):
79 log('Deferring action to CRM leader.', level=INFO)93 log('Deferring action to CRM leader.', level=INFO)
@@ -97,6 +111,7 @@
97 return False111 return False
98112
99113
114<<<<<<< TREE
100def is_crm_dc():115def is_crm_dc():
101 """116 """
102 Determine leadership by querying the pacemaker Designated Controller117 Determine leadership by querying the pacemaker Designated Controller
@@ -119,6 +134,35 @@
119134
120135
121@retry_on_exception(5, base_delay=2, exc_type=CRMResourceNotFound)136@retry_on_exception(5, base_delay=2, exc_type=CRMResourceNotFound)
137=======
138def is_crm_dc():
139 """
140 Determine leadership by querying the pacemaker Designated Controller
141 """
142 cmd = ['crm', 'status']
143 try:
144 status = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
145 if not isinstance(status, six.text_type):
146 status = six.text_type(status, "utf-8")
147 except subprocess.CalledProcessError as ex:
148 raise CRMDCNotFound(str(ex))
149
150 current_dc = ''
151 for line in status.split('\n'):
152 if line.startswith('Current DC'):
153 # Current DC: juju-lytrusty-machine-2 (168108163) - partition with quorum
154 current_dc = line.split(':')[1].split()[0]
155 if current_dc == get_unit_hostname():
156 return True
157 elif current_dc == 'NONE':
158 raise CRMDCNotFound('Current DC: NONE')
159
160 return False
161
162
163@retry_on_exception(5, base_delay=2,
164 exc_type=(CRMResourceNotFound, CRMDCNotFound))
165>>>>>>> MERGE-SOURCE
122def is_crm_leader(resource, retry=False):166def is_crm_leader(resource, retry=False):
123 """167 """
124 Returns True if the charm calling this is the elected corosync leader,168 Returns True if the charm calling this is the elected corosync leader,
125169
=== modified file 'hooks/charmhelpers/contrib/openstack/amulet/deployment.py'
--- hooks/charmhelpers/contrib/openstack/amulet/deployment.py 2015-04-23 14:52:07 +0000
+++ hooks/charmhelpers/contrib/openstack/amulet/deployment.py 2015-06-30 20:18:04 +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-03-20 17:15:02 +0000
+++ hooks/charmhelpers/contrib/openstack/amulet/utils.py 2015-06-30 20:18:04 +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:33:32 +0000
+++ hooks/charmhelpers/contrib/openstack/context.py 2015-06-30 20:18:04 +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/neutron.py'
--- hooks/charmhelpers/contrib/openstack/neutron.py 2015-04-16 19:53:49 +0000
+++ hooks/charmhelpers/contrib/openstack/neutron.py 2015-06-30 20:18:04 +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-04-16 19:53:49 +0000
+++ hooks/charmhelpers/contrib/openstack/utils.py 2015-06-30 20:18:04 +0000
@@ -53,9 +53,13 @@
53 get_ipv6_addr53 get_ipv6_addr
54)54)
5555
56from charmhelpers.contrib.python.packages import (
57 pip_create_virtualenv,
58 pip_install,
59)
60
56from charmhelpers.core.host import lsb_release, mounts, umount61from charmhelpers.core.host import lsb_release, mounts, umount
57from charmhelpers.fetch import apt_install, apt_cache, install_remote62from charmhelpers.fetch import apt_install, apt_cache, install_remote
58from charmhelpers.contrib.python.packages import pip_install
59from charmhelpers.contrib.storage.linux.utils import is_block_device, zap_disk63from charmhelpers.contrib.storage.linux.utils import is_block_device, zap_disk
60from charmhelpers.contrib.storage.linux.loopback import ensure_loopback_device64from charmhelpers.contrib.storage.linux.loopback import ensure_loopback_device
6165
@@ -75,6 +79,7 @@
75 ('trusty', 'icehouse'),79 ('trusty', 'icehouse'),
76 ('utopic', 'juno'),80 ('utopic', 'juno'),
77 ('vivid', 'kilo'),81 ('vivid', 'kilo'),
82 ('wily', 'liberty'),
78])83])
7984
8085
@@ -87,6 +92,7 @@
87 ('2014.1', 'icehouse'),92 ('2014.1', 'icehouse'),
88 ('2014.2', 'juno'),93 ('2014.2', 'juno'),
89 ('2015.1', 'kilo'),94 ('2015.1', 'kilo'),
95 ('2015.2', 'liberty'),
90])96])
9197
92# The ugly duckling98# The ugly duckling
@@ -109,6 +115,7 @@
109 ('2.2.0', 'juno'),115 ('2.2.0', 'juno'),
110 ('2.2.1', 'kilo'),116 ('2.2.1', 'kilo'),
111 ('2.2.2', 'kilo'),117 ('2.2.2', 'kilo'),
118 ('2.3.0', 'liberty'),
112])119])
113120
114DEFAULT_LOOPBACK_SIZE = '5G'121DEFAULT_LOOPBACK_SIZE = '5G'
@@ -317,6 +324,9 @@
317 'kilo': 'trusty-updates/kilo',324 'kilo': 'trusty-updates/kilo',
318 'kilo/updates': 'trusty-updates/kilo',325 'kilo/updates': 'trusty-updates/kilo',
319 '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',
320 }330 }
321331
322 try:332 try:
@@ -497,7 +507,17 @@
497requirements_dir = None507requirements_dir = None
498508
499509
500def git_clone_and_install(projects_yaml, core_project):510def _git_yaml_load(projects_yaml):
511 """
512 Load the specified yaml into a dictionary.
513 """
514 if not projects_yaml:
515 return None
516
517 return yaml.load(projects_yaml)
518
519
520def git_clone_and_install(projects_yaml, core_project, depth=1):
501 """521 """
502 Clone/install all specified OpenStack repositories.522 Clone/install all specified OpenStack repositories.
503523
@@ -510,23 +530,22 @@
510 repository: 'git://git.openstack.org/openstack/requirements.git',530 repository: 'git://git.openstack.org/openstack/requirements.git',
511 branch: 'stable/icehouse'}531 branch: 'stable/icehouse'}
512 directory: /mnt/openstack-git532 directory: /mnt/openstack-git
513 http_proxy: http://squid.internal:3128533 http_proxy: squid-proxy-url
514 https_proxy: https://squid.internal:3128534 https_proxy: squid-proxy-url
515535
516 The directory, http_proxy, and https_proxy keys are optional.536 The directory, http_proxy, and https_proxy keys are optional.
517 """537 """
518 global requirements_dir538 global requirements_dir
519 parent_dir = '/mnt/openstack-git'539 parent_dir = '/mnt/openstack-git'
520540 http_proxy = None
521 if not projects_yaml:541
522 return542 projects = _git_yaml_load(projects_yaml)
523
524 projects = yaml.load(projects_yaml)
525 _git_validate_projects_yaml(projects, core_project)543 _git_validate_projects_yaml(projects, core_project)
526544
527 old_environ = dict(os.environ)545 old_environ = dict(os.environ)
528546
529 if 'http_proxy' in projects.keys():547 if 'http_proxy' in projects.keys():
548 http_proxy = projects['http_proxy']
530 os.environ['http_proxy'] = projects['http_proxy']549 os.environ['http_proxy'] = projects['http_proxy']
531 if 'https_proxy' in projects.keys():550 if 'https_proxy' in projects.keys():
532 os.environ['https_proxy'] = projects['https_proxy']551 os.environ['https_proxy'] = projects['https_proxy']
@@ -534,15 +553,24 @@
534 if 'directory' in projects.keys():553 if 'directory' in projects.keys():
535 parent_dir = projects['directory']554 parent_dir = projects['directory']
536555
556 pip_create_virtualenv(os.path.join(parent_dir, 'venv'))
557
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
537 for p in projects['repositories']:563 for p in projects['repositories']:
538 repo = p['repository']564 repo = p['repository']
539 branch = p['branch']565 branch = p['branch']
540 if p['name'] == 'requirements':566 if p['name'] == 'requirements':
541 repo_dir = _git_clone_and_install_single(repo, branch, parent_dir,567 repo_dir = _git_clone_and_install_single(repo, branch, depth,
568 parent_dir, http_proxy,
542 update_requirements=False)569 update_requirements=False)
543 requirements_dir = repo_dir570 requirements_dir = repo_dir
544 else:571 else:
545 repo_dir = _git_clone_and_install_single(repo, branch, parent_dir,572 repo_dir = _git_clone_and_install_single(repo, branch, depth,
573 parent_dir, http_proxy,
546 update_requirements=True)574 update_requirements=True)
547575
548 os.environ = old_environ576 os.environ = old_environ
@@ -574,7 +602,8 @@
574 error_out('openstack-origin-git key \'{}\' is missing'.format(key))602 error_out('openstack-origin-git key \'{}\' is missing'.format(key))
575603
576604
577def _git_clone_and_install_single(repo, branch, parent_dir, update_requirements):605def _git_clone_and_install_single(repo, branch, depth, parent_dir, http_proxy,
606 update_requirements):
578 """607 """
579 Clone and install a single git repository.608 Clone and install a single git repository.
580 """609 """
@@ -587,23 +616,29 @@
587616
588 if not os.path.exists(dest_dir):617 if not os.path.exists(dest_dir):
589 juju_log('Cloning git repo: {}, branch: {}'.format(repo, branch))618 juju_log('Cloning git repo: {}, branch: {}'.format(repo, branch))
590 repo_dir = install_remote(repo, dest=parent_dir, branch=branch)619 repo_dir = install_remote(repo, dest=parent_dir, branch=branch,
620 depth=depth)
591 else:621 else:
592 repo_dir = dest_dir622 repo_dir = dest_dir
593623
624 venv = os.path.join(parent_dir, 'venv')
625
594 if update_requirements:626 if update_requirements:
595 if not requirements_dir:627 if not requirements_dir:
596 error_out('requirements repo must be cloned before '628 error_out('requirements repo must be cloned before '
597 'updating from global requirements.')629 'updating from global requirements.')
598 _git_update_requirements(repo_dir, requirements_dir)630 _git_update_requirements(venv, repo_dir, requirements_dir)
599631
600 juju_log('Installing git repo from dir: {}'.format(repo_dir))632 juju_log('Installing git repo from dir: {}'.format(repo_dir))
601 pip_install(repo_dir)633 if http_proxy:
634 pip_install(repo_dir, proxy=http_proxy, venv=venv)
635 else:
636 pip_install(repo_dir, venv=venv)
602637
603 return repo_dir638 return repo_dir
604639
605640
606def _git_update_requirements(package_dir, reqs_dir):641def _git_update_requirements(venv, package_dir, reqs_dir):
607 """642 """
608 Update from global requirements.643 Update from global requirements.
609644
@@ -612,25 +647,38 @@
612 """647 """
613 orig_dir = os.getcwd()648 orig_dir = os.getcwd()
614 os.chdir(reqs_dir)649 os.chdir(reqs_dir)
615 cmd = ['python', 'update.py', package_dir]650 python = os.path.join(venv, 'bin/python')
651 cmd = [python, 'update.py', package_dir]
616 try:652 try:
617 subprocess.check_call(cmd)653 subprocess.check_call(cmd)
618 except subprocess.CalledProcessError:654 except subprocess.CalledProcessError:
619 package = os.path.basename(package_dir)655 package = os.path.basename(package_dir)
620 error_out("Error updating {} from global-requirements.txt".format(package))656 error_out("Error updating {} from "
657 "global-requirements.txt".format(package))
621 os.chdir(orig_dir)658 os.chdir(orig_dir)
622659
623660
661def git_pip_venv_dir(projects_yaml):
662 """
663 Return the pip virtualenv path.
664 """
665 parent_dir = '/mnt/openstack-git'
666
667 projects = _git_yaml_load(projects_yaml)
668
669 if 'directory' in projects.keys():
670 parent_dir = projects['directory']
671
672 return os.path.join(parent_dir, 'venv')
673
674
624def git_src_dir(projects_yaml, project):675def git_src_dir(projects_yaml, project):
625 """676 """
626 Return the directory where the specified project's source is located.677 Return the directory where the specified project's source is located.
627 """678 """
628 parent_dir = '/mnt/openstack-git'679 parent_dir = '/mnt/openstack-git'
629680
630 if not projects_yaml:681 projects = _git_yaml_load(projects_yaml)
631 return
632
633 projects = yaml.load(projects_yaml)
634682
635 if 'directory' in projects.keys():683 if 'directory' in projects.keys():
636 parent_dir = projects['directory']684 parent_dir = projects['directory']
@@ -640,3 +688,15 @@
640 return os.path.join(parent_dir, os.path.basename(p['repository']))688 return os.path.join(parent_dir, os.path.basename(p['repository']))
641689
642 return None690 return None
691
692
693def git_yaml_value(projects_yaml, key):
694 """
695 Return the value in projects_yaml for the specified key.
696 """
697 projects = _git_yaml_load(projects_yaml)
698
699 if key in projects.keys():
700 return projects[key]
701
702 return None
643703
=== modified file 'hooks/charmhelpers/contrib/python/packages.py'
--- hooks/charmhelpers/contrib/python/packages.py 2015-03-20 17:15:02 +0000
+++ hooks/charmhelpers/contrib/python/packages.py 2015-06-30 20:18:04 +0000
@@ -17,8 +17,11 @@
17# You should have received a copy of the GNU Lesser General Public License17# You should have received a copy of the GNU Lesser General Public License
18# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.18# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1919
20import os
21import subprocess
22
20from charmhelpers.fetch import apt_install, apt_update23from charmhelpers.fetch import apt_install, apt_update
21from charmhelpers.core.hookenv import log24from charmhelpers.core.hookenv import charm_dir, log
2225
23try:26try:
24 from pip import main as pip_execute27 from pip import main as pip_execute
@@ -33,6 +36,8 @@
33def parse_options(given, available):36def parse_options(given, available):
34 """Given a set of options, check if available"""37 """Given a set of options, check if available"""
35 for key, value in sorted(given.items()):38 for key, value in sorted(given.items()):
39 if not value:
40 continue
36 if key in available:41 if key in available:
37 yield "--{0}={1}".format(key, value)42 yield "--{0}={1}".format(key, value)
3843
@@ -51,11 +56,15 @@
51 pip_execute(command)56 pip_execute(command)
5257
5358
54def pip_install(package, fatal=False, upgrade=False, **options):59def pip_install(package, fatal=False, upgrade=False, venv=None, **options):
55 """Install a python package"""60 """Install a python package"""
56 command = ["install"]61 if venv:
62 venv_python = os.path.join(venv, 'bin/pip')
63 command = [venv_python, "install"]
64 else:
65 command = ["install"]
5766
58 available_options = ('proxy', 'src', 'log', "index-url", )67 available_options = ('proxy', 'src', 'log', 'index-url', )
59 for option in parse_options(options, available_options):68 for option in parse_options(options, available_options):
60 command.append(option)69 command.append(option)
6170
@@ -69,7 +78,10 @@
6978
70 log("Installing {} package with options: {}".format(package,79 log("Installing {} package with options: {}".format(package,
71 command))80 command))
72 pip_execute(command)81 if venv:
82 subprocess.check_call(command)
83 else:
84 pip_execute(command)
7385
7486
75def pip_uninstall(package, **options):87def pip_uninstall(package, **options):
@@ -94,3 +106,16 @@
94 """Returns the list of current python installed packages106 """Returns the list of current python installed packages
95 """107 """
96 return pip_execute(["list"])108 return pip_execute(["list"])
109
110
111def pip_create_virtualenv(path=None):
112 """Create an isolated Python environment."""
113 apt_install('python-virtualenv')
114
115 if path:
116 venv_path = path
117 else:
118 venv_path = os.path.join(charm_dir(), 'venv')
119
120 if not os.path.exists(venv_path):
121 subprocess.check_call(['virtualenv', venv_path])
97122
=== modified file 'hooks/charmhelpers/core/hookenv.py'
--- hooks/charmhelpers/core/hookenv.py 2015-04-16 19:53:49 +0000
+++ hooks/charmhelpers/core/hookenv.py 2015-06-30 20:18:04 +0000
@@ -21,12 +21,16 @@
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 distutils.version import LooseVersion
25from functools import wraps
26import glob
24import os27import os
25import json28import json
26import yaml29import yaml
27import subprocess30import subprocess
28import sys31import sys
29import errno32import errno
33import tempfile
30from subprocess import CalledProcessError34from subprocess import CalledProcessError
3135
32import six36import six
@@ -58,15 +62,17 @@
5862
59 will cache the result of unit_get + 'test' for future calls.63 will cache the result of unit_get + 'test' for future calls.
60 """64 """
65 @wraps(func)
61 def wrapper(*args, **kwargs):66 def wrapper(*args, **kwargs):
62 global cache67 global cache
63 key = str((func, args, kwargs))68 key = str((func, args, kwargs))
64 try:69 try:
65 return cache[key]70 return cache[key]
66 except KeyError:71 except KeyError:
67 res = func(*args, **kwargs)72 pass # Drop out of the exception handler scope.
68 cache[key] = res73 res = func(*args, **kwargs)
69 return res74 cache[key] = res
75 return res
70 return wrapper76 return wrapper
7177
7278
@@ -178,7 +184,7 @@
178184
179def remote_unit():185def remote_unit():
180 """The remote unit for the current relation hook"""186 """The remote unit for the current relation hook"""
181 return os.environ['JUJU_REMOTE_UNIT']187 return os.environ.get('JUJU_REMOTE_UNIT', None)
182188
183189
184def service_name():190def service_name():
@@ -238,23 +244,7 @@
238 self.path = os.path.join(charm_dir(), Config.CONFIG_FILE_NAME)244 self.path = os.path.join(charm_dir(), Config.CONFIG_FILE_NAME)
239 if os.path.exists(self.path):245 if os.path.exists(self.path):
240 self.load_previous()246 self.load_previous()
241247 atexit(self._implicit_save)
242 def __getitem__(self, key):
243 """For regular dict lookups, check the current juju config first,
244 then the previous (saved) copy. This ensures that user-saved values
245 will be returned by a dict lookup.
246
247 """
248 try:
249 return dict.__getitem__(self, key)
250 except KeyError:
251 return (self._prev_dict or {})[key]
252
253 def keys(self):
254 prev_keys = []
255 if self._prev_dict is not None:
256 prev_keys = self._prev_dict.keys()
257 return list(set(prev_keys + list(dict.keys(self))))
258248
259 def load_previous(self, path=None):249 def load_previous(self, path=None):
260 """Load previous copy of config from disk.250 """Load previous copy of config from disk.
@@ -273,6 +263,9 @@
273 self.path = path or self.path263 self.path = path or self.path
274 with open(self.path) as f:264 with open(self.path) as f:
275 self._prev_dict = json.load(f)265 self._prev_dict = json.load(f)
266 for k, v in self._prev_dict.items():
267 if k not in self:
268 self[k] = v
276269
277 def changed(self, key):270 def changed(self, key):
278 """Return True if the current value for this key is different from271 """Return True if the current value for this key is different from
@@ -304,13 +297,13 @@
304 instance.297 instance.
305298
306 """299 """
307 if self._prev_dict:
308 for k, v in six.iteritems(self._prev_dict):
309 if k not in self:
310 self[k] = v
311 with open(self.path, 'w') as f:300 with open(self.path, 'w') as f:
312 json.dump(self, f)301 json.dump(self, f)
313302
303 def _implicit_save(self):
304 if self.implicit_save:
305 self.save()
306
314307
315@cached308@cached
316def config(scope=None):309def config(scope=None):
@@ -353,18 +346,49 @@
353 """Set relation information for the current unit"""346 """Set relation information for the current unit"""
354 relation_settings = relation_settings if relation_settings else {}347 relation_settings = relation_settings if relation_settings else {}
355 relation_cmd_line = ['relation-set']348 relation_cmd_line = ['relation-set']
349 accepts_file = "--file" in subprocess.check_output(
350 relation_cmd_line + ["--help"], universal_newlines=True)
356 if relation_id is not None:351 if relation_id is not None:
357 relation_cmd_line.extend(('-r', relation_id))352 relation_cmd_line.extend(('-r', relation_id))
358 for k, v in (list(relation_settings.items()) + list(kwargs.items())):353 settings = relation_settings.copy()
359 if v is None:354 settings.update(kwargs)
360 relation_cmd_line.append('{}='.format(k))355 for key, value in settings.items():
361 else:356 # Force value to be a string: it always should, but some call
362 relation_cmd_line.append('{}={}'.format(k, v))357 # sites pass in things like dicts or numbers.
363 subprocess.check_call(relation_cmd_line)358 if value is not None:
359 settings[key] = "{}".format(value)
360 if accepts_file:
361 # --file was introduced in Juju 1.23.2. Use it by default if
362 # available, since otherwise we'll break if the relation data is
363 # too big. Ideally we should tell relation-set to read the data from
364 # stdin, but that feature is broken in 1.23.2: Bug #1454678.
365 with tempfile.NamedTemporaryFile(delete=False) as settings_file:
366 settings_file.write(yaml.safe_dump(settings).encode("utf-8"))
367 subprocess.check_call(
368 relation_cmd_line + ["--file", settings_file.name])
369 os.remove(settings_file.name)
370 else:
371 for key, value in settings.items():
372 if value is None:
373 relation_cmd_line.append('{}='.format(key))
374 else:
375 relation_cmd_line.append('{}={}'.format(key, value))
376 subprocess.check_call(relation_cmd_line)
364 # Flush cache of any relation-gets for local unit377 # Flush cache of any relation-gets for local unit
365 flush(local_unit())378 flush(local_unit())
366379
367380
381def relation_clear(r_id=None):
382 ''' Clears any relation data already set on relation r_id '''
383 settings = relation_get(rid=r_id,
384 unit=local_unit())
385 for setting in settings:
386 if setting not in ['public-address', 'private-address']:
387 settings[setting] = None
388 relation_set(relation_id=r_id,
389 **settings)
390
391
368@cached392@cached
369def relation_ids(reltype=None):393def relation_ids(reltype=None):
370 """A list of relation_ids"""394 """A list of relation_ids"""
@@ -509,6 +533,11 @@
509 return None533 return None
510534
511535
536def unit_public_ip():
537 """Get this unit's public IP address"""
538 return unit_get('public-address')
539
540
512def unit_private_ip():541def unit_private_ip():
513 """Get this unit's private IP address"""542 """Get this unit's private IP address"""
514 return unit_get('private-address')543 return unit_get('private-address')
@@ -541,10 +570,14 @@
541 hooks.execute(sys.argv)570 hooks.execute(sys.argv)
542 """571 """
543572
544 def __init__(self, config_save=True):573 def __init__(self, config_save=None):
545 super(Hooks, self).__init__()574 super(Hooks, self).__init__()
546 self._hooks = {}575 self._hooks = {}
547 self._config_save = config_save576
577 # For unknown reasons, we allow the Hooks constructor to override
578 # config().implicit_save.
579 if config_save is not None:
580 config().implicit_save = config_save
548581
549 def register(self, name, function):582 def register(self, name, function):
550 """Register a hook"""583 """Register a hook"""
@@ -552,13 +585,16 @@
552585
553 def execute(self, args):586 def execute(self, args):
554 """Execute a registered hook based on args[0]"""587 """Execute a registered hook based on args[0]"""
588 _run_atstart()
555 hook_name = os.path.basename(args[0])589 hook_name = os.path.basename(args[0])
556 if hook_name in self._hooks:590 if hook_name in self._hooks:
557 self._hooks[hook_name]()591 try:
558 if self._config_save:592 self._hooks[hook_name]()
559 cfg = config()593 except SystemExit as x:
560 if cfg.implicit_save:594 if x.code is None or x.code == 0:
561 cfg.save()595 _run_atexit()
596 raise
597 _run_atexit()
562 else:598 else:
563 raise UnregisteredHookError(hook_name)599 raise UnregisteredHookError(hook_name)
564600
@@ -605,3 +641,160 @@
605641
606 The results set by action_set are preserved."""642 The results set by action_set are preserved."""
607 subprocess.check_call(['action-fail', message])643 subprocess.check_call(['action-fail', message])
644
645
646def status_set(workload_state, message):
647 """Set the workload state with a message
648
649 Use status-set to set the workload state with a message which is visible
650 to the user via juju status. If the status-set command is not found then
651 assume this is juju < 1.23 and juju-log the message unstead.
652
653 workload_state -- valid juju workload state.
654 message -- status update message
655 """
656 valid_states = ['maintenance', 'blocked', 'waiting', 'active']
657 if workload_state not in valid_states:
658 raise ValueError(
659 '{!r} is not a valid workload state'.format(workload_state)
660 )
661 cmd = ['status-set', workload_state, message]
662 try:
663 ret = subprocess.call(cmd)
664 if ret == 0:
665 return
666 except OSError as e:
667 if e.errno != errno.ENOENT:
668 raise
669 log_message = 'status-set failed: {} {}'.format(workload_state,
670 message)
671 log(log_message, level='INFO')
672
673
674def status_get():
675 """Retrieve the previously set juju workload state
676
677 If the status-set command is not found then assume this is juju < 1.23 and
678 return 'unknown'
679 """
680 cmd = ['status-get']
681 try:
682 raw_status = subprocess.check_output(cmd, universal_newlines=True)
683 status = raw_status.rstrip()
684 return status
685 except OSError as e:
686 if e.errno == errno.ENOENT:
687 return 'unknown'
688 else:
689 raise
690
691
692def translate_exc(from_exc, to_exc):
693 def inner_translate_exc1(f):
694 def inner_translate_exc2(*args, **kwargs):
695 try:
696 return f(*args, **kwargs)
697 except from_exc:
698 raise to_exc
699
700 return inner_translate_exc2
701
702 return inner_translate_exc1
703
704
705@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
706def is_leader():
707 """Does the current unit hold the juju leadership
708
709 Uses juju to determine whether the current unit is the leader of its peers
710 """
711 cmd = ['is-leader', '--format=json']
712 return json.loads(subprocess.check_output(cmd).decode('UTF-8'))
713
714
715@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
716def leader_get(attribute=None):
717 """Juju leader get value(s)"""
718 cmd = ['leader-get', '--format=json'] + [attribute or '-']
719 return json.loads(subprocess.check_output(cmd).decode('UTF-8'))
720
721
722@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
723def leader_set(settings=None, **kwargs):
724 """Juju leader set value(s)"""
725 # Don't log secrets.
726 # log("Juju leader-set '%s'" % (settings), level=DEBUG)
727 cmd = ['leader-set']
728 settings = settings or {}
729 settings.update(kwargs)
730 for k, v in settings.items():
731 if v is None:
732 cmd.append('{}='.format(k))
733 else:
734 cmd.append('{}={}'.format(k, v))
735 subprocess.check_call(cmd)
736
737
738@cached
739def juju_version():
740 """Full version string (eg. '1.23.3.1-trusty-amd64')"""
741 # Per https://bugs.launchpad.net/juju-core/+bug/1455368/comments/1
742 jujud = glob.glob('/var/lib/juju/tools/machine-*/jujud')[0]
743 return subprocess.check_output([jujud, 'version'],
744 universal_newlines=True).strip()
745
746
747@cached
748def has_juju_version(minimum_version):
749 """Return True if the Juju version is at least the provided version"""
750 return LooseVersion(juju_version()) >= LooseVersion(minimum_version)
751
752
753_atexit = []
754_atstart = []
755
756
757def atstart(callback, *args, **kwargs):
758 '''Schedule a callback to run before the main hook.
759
760 Callbacks are run in the order they were added.
761
762 This is useful for modules and classes to perform initialization
763 and inject behavior. In particular:
764 - Run common code before all of your hooks, such as logging
765 the hook name or interesting relation data.
766 - Defer object or module initialization that requires a hook
767 context until we know there actually is a hook context,
768 making testing easier.
769 - Rather than requiring charm authors to include boilerplate to
770 invoke your helper's behavior, have it run automatically if
771 your object is instantiated or module imported.
772
773 This is not at all useful after your hook framework as been launched.
774 '''
775 global _atstart
776 _atstart.append((callback, args, kwargs))
777
778
779def atexit(callback, *args, **kwargs):
780 '''Schedule a callback to run on successful hook completion.
781
782 Callbacks are run in the reverse order that they were added.'''
783 _atexit.append((callback, args, kwargs))
784
785
786def _run_atstart():
787 '''Hook frameworks must invoke this before running the main hook body.'''
788 global _atstart
789 for callback, args, kwargs in _atstart:
790 callback(*args, **kwargs)
791 del _atstart[:]
792
793
794def _run_atexit():
795 '''Hook frameworks must invoke this after the main hook body has
796 successfully completed. Do not invoke it if the hook fails.'''
797 global _atexit
798 for callback, args, kwargs in reversed(_atexit):
799 callback(*args, **kwargs)
800 del _atexit[:]
608801
=== modified file 'hooks/charmhelpers/core/host.py'
--- hooks/charmhelpers/core/host.py 2015-03-20 17:15:02 +0000
+++ hooks/charmhelpers/core/host.py 2015-06-30 20:18:04 +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-03-20 17:15:02 +0000
+++ hooks/charmhelpers/core/services/base.py 2015-06-30 20:18:04 +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
@@ -128,15 +128,18 @@
128 """128 """
129 Handle the current hook by doing The Right Thing with the registered services.129 Handle the current hook by doing The Right Thing with the registered services.
130 """130 """
131 hook_name = hookenv.hook_name()131 hookenv._run_atstart()
132 if hook_name == 'stop':132 try:
133 self.stop_services()133 hook_name = hookenv.hook_name()
134 else:134 if hook_name == 'stop':
135 self.provide_data()135 self.stop_services()
136 self.reconfigure_services()136 else:
137 cfg = hookenv.config()137 self.reconfigure_services()
138 if cfg.implicit_save:138 self.provide_data()
139 cfg.save()139 except SystemExit as x:
140 if x.code is None or x.code == 0:
141 hookenv._run_atexit()
142 hookenv._run_atexit()
140143
141 def provide_data(self):144 def provide_data(self):
142 """145 """
@@ -145,15 +148,36 @@
145 A provider must have a `name` attribute, which indicates which relation148 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 of149 to set data on, and a `provide_data()` method, which returns a dict of
147 data to set.150 data to set.
151
152 The `provide_data()` method can optionally accept two parameters:
153
154 * ``remote_service`` The name of the remote service that the data will
155 be provided to. The `provide_data()` method will be called once
156 for each connected service (not unit). This allows the method to
157 tailor its data to the given service.
158 * ``service_ready`` Whether or not the service definition had all of
159 its requirements met, and thus the ``data_ready`` callbacks run.
160
161 Note that the ``provided_data`` methods are now called **after** the
162 ``data_ready`` callbacks are run. This gives the ``data_ready`` callbacks
163 a chance to generate any data necessary for the providing to the remote
164 services.
148 """165 """
149 hook_name = hookenv.hook_name()166 for service_name, service in self.services.items():
150 for service in self.services.values():167 service_ready = self.is_ready(service_name)
151 for provider in service.get('provided_data', []):168 for provider in service.get('provided_data', []):
152 if re.match(r'{}-relation-(joined|changed)'.format(provider.name), hook_name):169 for relid in hookenv.relation_ids(provider.name):
153 data = provider.provide_data()170 units = hookenv.related_units(relid)
154 _ready = provider._is_ready(data) if hasattr(provider, '_is_ready') else data171 if not units:
155 if _ready:172 continue
156 hookenv.relation_set(None, data)173 remote_service = units[0].split('/')[0]
174 argspec = getargspec(provider.provide_data)
175 if len(argspec.args) > 1:
176 data = provider.provide_data(remote_service, service_ready)
177 else:
178 data = provider.provide_data()
179 if data:
180 hookenv.relation_set(relid, data)
157181
158 def reconfigure_services(self, *service_names):182 def reconfigure_services(self, *service_names):
159 """183 """
160184
=== modified file 'hooks/charmhelpers/fetch/__init__.py'
--- hooks/charmhelpers/fetch/__init__.py 2015-03-20 17:15:02 +0000
+++ hooks/charmhelpers/fetch/__init__.py 2015-06-30 20:18:04 +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 'hooks/charmhelpers/fetch/giturl.py'
--- hooks/charmhelpers/fetch/giturl.py 2015-03-20 17:15:02 +0000
+++ hooks/charmhelpers/fetch/giturl.py 2015-06-30 20:18:04 +0000
@@ -45,14 +45,16 @@
45 else:45 else:
46 return True46 return True
4747
48 def clone(self, source, dest, branch):48 def clone(self, source, dest, branch, depth=None):
49 if not self.can_handle(source):49 if not self.can_handle(source):
50 raise UnhandledSource("Cannot handle {}".format(source))50 raise UnhandledSource("Cannot handle {}".format(source))
5151
52 repo = Repo.clone_from(source, dest)52 if depth:
53 repo.git.checkout(branch)53 Repo.clone_from(source, dest, branch=branch, depth=depth)
54 else:
55 Repo.clone_from(source, dest, branch=branch)
5456
55 def install(self, source, branch="master", dest=None):57 def install(self, source, branch="master", dest=None, depth=None):
56 url_parts = self.parse_url(source)58 url_parts = self.parse_url(source)
57 branch_name = url_parts.path.strip("/").split("/")[-1]59 branch_name = url_parts.path.strip("/").split("/")[-1]
58 if dest:60 if dest:
@@ -63,7 +65,7 @@
63 if not os.path.exists(dest_dir):65 if not os.path.exists(dest_dir):
64 mkdir(dest_dir, perms=0o755)66 mkdir(dest_dir, perms=0o755)
65 try:67 try:
66 self.clone(source, dest_dir, branch)68 self.clone(source, dest_dir, branch, depth)
67 except GitCommandError as e:69 except GitCommandError as e:
68 raise UnhandledSource(e.message)70 raise UnhandledSource(e.message)
69 except OSError as e:71 except OSError as e:
7072
=== modified file 'hooks/glance_relations.py'
--- hooks/glance_relations.py 2015-05-01 11:37:27 +0000
+++ hooks/glance_relations.py 2015-06-30 20:18:04 +0000
@@ -53,7 +53,7 @@
53 filter_installed_packages53 filter_installed_packages
54)54)
55from charmhelpers.contrib.hahelpers.cluster import (55from charmhelpers.contrib.hahelpers.cluster import (
56 eligible_leader,56 is_elected_leader,
57 get_hacluster_config57 get_hacluster_config
58)58)
59from charmhelpers.contrib.openstack.utils import (59from charmhelpers.contrib.openstack.utils import (
@@ -160,7 +160,7 @@
160 if rel != "essex":160 if rel != "essex":
161 CONFIGS.write(GLANCE_API_CONF)161 CONFIGS.write(GLANCE_API_CONF)
162162
163 if eligible_leader(CLUSTER_RES):163 if is_elected_leader(CLUSTER_RES):
164 # Bugs 1353135 & 1187508. Dbs can appear to be ready before the units164 # Bugs 1353135 & 1187508. Dbs can appear to be ready before the units
165 # acl entry has been added. So, if the db supports passing a list of165 # acl entry has been added. So, if the db supports passing a list of
166 # permitted units then check if we're in the list.166 # permitted units then check if we're in the list.
@@ -194,7 +194,7 @@
194 if rel != "essex":194 if rel != "essex":
195 CONFIGS.write(GLANCE_API_CONF)195 CONFIGS.write(GLANCE_API_CONF)
196196
197 if eligible_leader(CLUSTER_RES):197 if is_elected_leader(CLUSTER_RES):
198 if rel == "essex":198 if rel == "essex":
199 status = call(['glance-manage', 'db_version'])199 status = call(['glance-manage', 'db_version'])
200 if status != 0:200 if status != 0:
201201
=== modified file 'hooks/glance_utils.py'
--- hooks/glance_utils.py 2015-04-17 12:05:48 +0000
+++ hooks/glance_utils.py 2015-06-30 20:18:04 +0000
@@ -14,6 +14,10 @@
14 apt_install,14 apt_install,
15 add_source)15 add_source)
1616
17from charmhelpers.contrib.python.packages import (
18 pip_install,
19)
20
17from charmhelpers.core.hookenv import (21from charmhelpers.core.hookenv import (
18 charm_dir,22 charm_dir,
19 config,23 config,
@@ -38,7 +42,7 @@
38 context,)42 context,)
3943
40from charmhelpers.contrib.hahelpers.cluster import (44from charmhelpers.contrib.hahelpers.cluster import (
41 eligible_leader,45 is_elected_leader,
42)46)
4347
44from charmhelpers.contrib.openstack.alternatives import install_alternative48from charmhelpers.contrib.openstack.alternatives import install_alternative
@@ -47,12 +51,18 @@
47 git_install_requested,51 git_install_requested,
48 git_clone_and_install,52 git_clone_and_install,
49 git_src_dir,53 git_src_dir,
54 git_yaml_value,
55 git_pip_venv_dir,
50 configure_installation_source,56 configure_installation_source,
51 os_release,57 os_release,
52)58)
5359
54from charmhelpers.core.templating import render60from charmhelpers.core.templating import render
5561
62from charmhelpers.core.decorators import (
63 retry_on_exception,
64)
65
56CLUSTER_RES = "grp_glance_vips"66CLUSTER_RES = "grp_glance_vips"
5767
58PACKAGES = [68PACKAGES = [
@@ -60,8 +70,12 @@
60 "python-psycopg2", "python-keystone", "python-six", "uuid", "haproxy", ]70 "python-psycopg2", "python-keystone", "python-six", "uuid", "haproxy", ]
6171
62BASE_GIT_PACKAGES = [72BASE_GIT_PACKAGES = [
73 'libffi-dev',
74 'libmysqlclient-dev',
63 'libxml2-dev',75 'libxml2-dev',
64 'libxslt1-dev',76 'libxslt1-dev',
77 'libssl-dev',
78 'libyaml-dev',
65 'python-dev',79 'python-dev',
66 'python-pip',80 'python-pip',
67 'python-setuptools',81 'python-setuptools',
@@ -209,6 +223,9 @@
209 return configs223 return configs
210224
211225
226# NOTE(jamespage): Retry deals with sync issues during one-shot HA deploys.
227# mysql might be restarting or suchlike.
228@retry_on_exception(5, base_delay=3, exc_type=subprocess.CalledProcessError)
212def determine_packages():229def determine_packages():
213 packages = [] + PACKAGES230 packages = [] + PACKAGES
214231
@@ -256,7 +273,7 @@
256 configs.write_all()273 configs.write_all()
257274
258 [service_stop(s) for s in services()]275 [service_stop(s) for s in services()]
259 if eligible_leader(CLUSTER_RES):276 if is_elected_leader(CLUSTER_RES):
260 migrate_database()277 migrate_database()
261 [service_start(s) for s in services()]278 [service_start(s) for s in services()]
262279
@@ -340,6 +357,14 @@
340357
341def git_post_install(projects_yaml):358def git_post_install(projects_yaml):
342 """Perform glance post-install setup."""359 """Perform glance post-install setup."""
360 http_proxy = git_yaml_value(projects_yaml, 'http_proxy')
361 if http_proxy:
362 pip_install('mysql-python', proxy=http_proxy,
363 venv=git_pip_venv_dir(projects_yaml))
364 else:
365 pip_install('mysql-python',
366 venv=git_pip_venv_dir(projects_yaml))
367
343 src_etc = os.path.join(git_src_dir(projects_yaml, 'glance'), 'etc')368 src_etc = os.path.join(git_src_dir(projects_yaml, 'glance'), 'etc')
344 configs = {369 configs = {
345 'src': src_etc,370 'src': src_etc,
@@ -350,13 +375,34 @@
350 shutil.rmtree(configs['dest'])375 shutil.rmtree(configs['dest'])
351 shutil.copytree(configs['src'], configs['dest'])376 shutil.copytree(configs['src'], configs['dest'])
352377
378 symlinks = [
379 # NOTE(coreycb): Need to find better solution than bin symlinks.
380 {'src': os.path.join(git_pip_venv_dir(projects_yaml),
381 'bin/glance-manage'),
382 'link': '/usr/local/bin/glance-manage'},
383 # NOTE(coreycb): This is ugly but couldn't find pypi package that
384 # installs rbd.py and rados.py.
385 {'src': '/usr/lib/python2.7/dist-packages/rbd.py',
386 'link': os.path.join(git_pip_venv_dir(projects_yaml),
387 'lib/python2.7/site-packages/rbd.py')},
388 {'src': '/usr/lib/python2.7/dist-packages/rados.py',
389 'link': os.path.join(git_pip_venv_dir(projects_yaml),
390 'lib/python2.7/site-packages/rados.py')},
391 ]
392
393 for s in symlinks:
394 if os.path.lexists(s['link']):
395 os.remove(s['link'])
396 os.symlink(s['src'], s['link'])
397
398 bin_dir = os.path.join(git_pip_venv_dir(projects_yaml), 'bin')
353 glance_api_context = {399 glance_api_context = {
354 'service_description': 'Glance API server',400 'service_description': 'Glance API server',
355 'service_name': 'Glance',401 'service_name': 'Glance',
356 'user_name': 'glance',402 'user_name': 'glance',
357 'start_dir': '/var/lib/glance',403 'start_dir': '/var/lib/glance',
358 'process_name': 'glance-api',404 'process_name': 'glance-api',
359 'executable_name': '/usr/local/bin/glance-api',405 'executable_name': os.path.join(bin_dir, 'glance-api'),
360 'config_files': ['/etc/glance/glance-api.conf'],406 'config_files': ['/etc/glance/glance-api.conf'],
361 'log_file': '/var/log/glance/api.log',407 'log_file': '/var/log/glance/api.log',
362 }408 }
@@ -367,7 +413,7 @@
367 'user_name': 'glance',413 'user_name': 'glance',
368 'start_dir': '/var/lib/glance',414 'start_dir': '/var/lib/glance',
369 'process_name': 'glance-registry',415 'process_name': 'glance-registry',
370 'executable_name': '/usr/local/bin/glance-registry',416 'executable_name': os.path.join(bin_dir, 'glance-registry'),
371 'config_files': ['/etc/glance/glance-registry.conf'],417 'config_files': ['/etc/glance/glance-registry.conf'],
372 'log_file': '/var/log/glance/registry.log',418 'log_file': '/var/log/glance/registry.log',
373 }419 }
374420
=== modified file 'metadata.yaml'
--- metadata.yaml 2014-10-30 03:30:35 +0000
+++ metadata.yaml 2015-06-30 20:18:04 +0000
@@ -6,7 +6,7 @@
6 (Parallax) and an image delivery service (Teller). These services are used6 (Parallax) and an image delivery service (Teller). These services are used
7 in conjunction by Nova to deliver images from object stores, such as7 in conjunction by Nova to deliver images from object stores, such as
8 OpenStack's Swift service, to Nova's compute nodes.8 OpenStack's Swift service, to Nova's compute nodes.
9categories:9tags:
10 - miscellaneous10 - miscellaneous
11provides:11provides:
12 nrpe-external-master:12 nrpe-external-master:
1313
=== modified file 'tests/00-setup'
--- tests/00-setup 2014-10-08 20:18:38 +0000
+++ tests/00-setup 2015-06-30 20:18:04 +0000
@@ -5,6 +5,10 @@
5sudo add-apt-repository --yes ppa:juju/stable5sudo add-apt-repository --yes ppa:juju/stable
6sudo apt-get update --yes6sudo apt-get update --yes
7sudo apt-get install --yes python-amulet \7sudo apt-get install --yes python-amulet \
8 python-cinderclient \
9 python-distro-info \
10 python-glanceclient \
11 python-heatclient \
8 python-keystoneclient \12 python-keystoneclient \
9 python-glanceclient \
10 python-novaclient13 python-novaclient
14 python-swiftclient
1115
=== modified file 'tests/017-basic-trusty-kilo' (properties changed: -x to +x)
=== modified file 'tests/019-basic-vivid-kilo' (properties changed: -x to +x)
=== added file 'tests/020-basic-trusty-liberty'
--- tests/020-basic-trusty-liberty 1970-01-01 00:00:00 +0000
+++ tests/020-basic-trusty-liberty 2015-06-30 20:18:04 +0000
@@ -0,0 +1,11 @@
1#!/usr/bin/python
2
3"""Amulet tests on a basic glance deployment on trusty-liberty."""
4
5from basic_deployment import GlanceBasicDeployment
6
7if __name__ == '__main__':
8 deployment = GlanceBasicDeployment(series='trusty',
9 openstack='cloud:trusty-liberty',
10 source='cloud:trusty-updates/liberty')
11 deployment.run_tests()
012
=== added file 'tests/021-basic-wily-liberty'
--- tests/021-basic-wily-liberty 1970-01-01 00:00:00 +0000
+++ tests/021-basic-wily-liberty 2015-06-30 20:18:04 +0000
@@ -0,0 +1,9 @@
1#!/usr/bin/python
2
3"""Amulet tests on a basic glance deployment on wily-liberty."""
4
5from basic_deployment import GlanceBasicDeployment
6
7if __name__ == '__main__':
8 deployment = GlanceBasicDeployment(series='wily')
9 deployment.run_tests()
010
=== modified file 'tests/README'
--- tests/README 2014-10-08 20:18:38 +0000
+++ tests/README 2015-06-30 20:18:04 +0000
@@ -1,6 +1,15 @@
1This directory provides Amulet tests that focus on verification of Glance1This directory provides Amulet tests that focus on verification of Glance
2deployments.2deployments.
33
4test_* methods are called in lexical sort order.
5
6Test name convention to ensure desired test order:
7 1xx service and endpoint checks
8 2xx relation checks
9 3xx config checks
10 4xx functional checks
11 9xx restarts and other final checks
12
4In order to run tests, you'll need charm-tools installed (in addition to13In order to run tests, you'll need charm-tools installed (in addition to
5juju, of course):14juju, of course):
6 sudo add-apt-repository ppa:juju/stable15 sudo add-apt-repository ppa:juju/stable
716
=== modified file 'tests/basic_deployment.py'
--- tests/basic_deployment.py 2015-04-24 10:07:08 +0000
+++ tests/basic_deployment.py 2015-06-30 20:18:04 +0000
@@ -2,6 +2,7 @@
22
3import amulet3import amulet
4import os4import os
5import time
5import yaml6import yaml
67
7from charmhelpers.contrib.openstack.amulet.deployment import (8from charmhelpers.contrib.openstack.amulet.deployment import (
@@ -10,25 +11,30 @@
1011
11from charmhelpers.contrib.openstack.amulet.utils import (12from charmhelpers.contrib.openstack.amulet.utils import (
12 OpenStackAmuletUtils,13 OpenStackAmuletUtils,
13 DEBUG, # flake8: noqa14 DEBUG,
14 ERROR15 # ERROR
15)16)
1617
17# Use DEBUG to turn on debug logging18# Use DEBUG to turn on debug logging
18u = OpenStackAmuletUtils(DEBUG)19u = OpenStackAmuletUtils(DEBUG)
1920
21
20class GlanceBasicDeployment(OpenStackAmuletDeployment):22class GlanceBasicDeployment(OpenStackAmuletDeployment):
21 '''Amulet tests on a basic file-backed glance deployment. Verify relations,23 """Amulet tests on a basic file-backed glance deployment. Verify
22 service status, endpoint service catalog, create and delete new image.'''24 relations, service status, endpoint service catalog, create and
2325 delete new image."""
24# TO-DO(beisner):
25# * Add tests with different storage back ends
26# * Resolve Essex->Havana juju set charm bug
2726
28 def __init__(self, series=None, openstack=None, source=None, git=False,27 def __init__(self, series=None, openstack=None, source=None, git=False,
28<<<<<<< TREE
29 stable=True):29 stable=True):
30 '''Deploy the entire test environment.'''30 '''Deploy the entire test environment.'''
31 super(GlanceBasicDeployment, self).__init__(series, openstack, source, stable)31 super(GlanceBasicDeployment, self).__init__(series, openstack, source, stable)
32=======
33 stable=False):
34 """Deploy the entire test environment."""
35 super(GlanceBasicDeployment, self).__init__(series, openstack,
36 source, stable)
37>>>>>>> MERGE-SOURCE
32 self.git = git38 self.git = git
33 self._add_services()39 self._add_services()
34 self._add_relations()40 self._add_relations()
@@ -37,20 +43,21 @@
37 self._initialize_tests()43 self._initialize_tests()
3844
39 def _add_services(self):45 def _add_services(self):
40 '''Add services46 """Add services
4147
42 Add the services that we're testing, where glance is local,48 Add the services that we're testing, where glance is local,
43 and the rest of the service are from lp branches that are49 and the rest of the service are from lp branches that are
44 compatible with the local charm (e.g. stable or next).50 compatible with the local charm (e.g. stable or next).
45 '''51 """
46 this_service = {'name': 'glance'}52 this_service = {'name': 'glance'}
47 other_services = [{'name': 'mysql'}, {'name': 'rabbitmq-server'},53 other_services = [{'name': 'mysql'},
54 {'name': 'rabbitmq-server'},
48 {'name': 'keystone'}]55 {'name': 'keystone'}]
49 super(GlanceBasicDeployment, self)._add_services(this_service,56 super(GlanceBasicDeployment, self)._add_services(this_service,
50 other_services)57 other_services)
5158
52 def _add_relations(self):59 def _add_relations(self):
53 '''Add relations for the services.'''60 """Add relations for the services."""
54 relations = {'glance:identity-service': 'keystone:identity-service',61 relations = {'glance:identity-service': 'keystone:identity-service',
55 'glance:shared-db': 'mysql:shared-db',62 'glance:shared-db': 'mysql:shared-db',
56 'keystone:shared-db': 'mysql:shared-db',63 'keystone:shared-db': 'mysql:shared-db',
@@ -58,7 +65,7 @@
58 super(GlanceBasicDeployment, self)._add_relations(relations)65 super(GlanceBasicDeployment, self)._add_relations(relations)
5966
60 def _configure_services(self):67 def _configure_services(self):
61 '''Configure all of the services.'''68 """Configure all of the services."""
62 glance_config = {}69 glance_config = {}
63 if self.git:70 if self.git:
64 branch = 'stable/' + self._get_openstack_release_string()71 branch = 'stable/' + self._get_openstack_release_string()
@@ -66,17 +73,18 @@
66 openstack_origin_git = {73 openstack_origin_git = {
67 'repositories': [74 'repositories': [
68 {'name': 'requirements',75 {'name': 'requirements',
69 'repository': 'git://git.openstack.org/openstack/requirements',76 'repository': 'git://github.com/openstack/requirements',
70 'branch': branch},77 'branch': branch},
71 {'name': 'glance',78 {'name': 'glance',
72 'repository': 'git://git.openstack.org/openstack/glance',79 'repository': 'git://github.com/openstack/glance',
73 'branch': branch},80 'branch': branch},
74 ],81 ],
75 'directory': '/mnt/openstack-git',82 'directory': '/mnt/openstack-git',
76 'http_proxy': amulet_http_proxy,83 'http_proxy': amulet_http_proxy,
77 'https_proxy': amulet_http_proxy,84 'https_proxy': amulet_http_proxy,
78 }85 }
79 glance_config['openstack-origin-git'] = yaml.dump(openstack_origin_git)86 glance_config['openstack-origin-git'] = \
87 yaml.dump(openstack_origin_git)
8088
81 keystone_config = {'admin-password': 'openstack',89 keystone_config = {'admin-password': 'openstack',
82 'admin-token': 'ubuntutesting'}90 'admin-token': 'ubuntutesting'}
@@ -87,12 +95,19 @@
87 super(GlanceBasicDeployment, self)._configure_services(configs)95 super(GlanceBasicDeployment, self)._configure_services(configs)
8896
89 def _initialize_tests(self):97 def _initialize_tests(self):
90 '''Perform final initialization before tests get run.'''98 """Perform final initialization before tests get run."""
91 # Access the sentries for inspecting service units99 # Access the sentries for inspecting service units
92 self.mysql_sentry = self.d.sentry.unit['mysql/0']100 self.mysql_sentry = self.d.sentry.unit['mysql/0']
93 self.glance_sentry = self.d.sentry.unit['glance/0']101 self.glance_sentry = self.d.sentry.unit['glance/0']
94 self.keystone_sentry = self.d.sentry.unit['keystone/0']102 self.keystone_sentry = self.d.sentry.unit['keystone/0']
95 self.rabbitmq_sentry = self.d.sentry.unit['rabbitmq-server/0']103 self.rabbitmq_sentry = self.d.sentry.unit['rabbitmq-server/0']
104 u.log.debug('openstack release val: {}'.format(
105 self._get_openstack_release()))
106 u.log.debug('openstack release str: {}'.format(
107 self._get_openstack_release_string()))
108
109 # Let things settle a bit before moving forward
110 time.sleep(30)
96111
97 # Authenticate admin with keystone112 # Authenticate admin with keystone
98 self.keystone = u.authenticate_keystone_admin(self.keystone_sentry,113 self.keystone = u.authenticate_keystone_admin(self.keystone_sentry,
@@ -103,46 +118,99 @@
103 # Authenticate admin with glance endpoint118 # Authenticate admin with glance endpoint
104 self.glance = u.authenticate_glance_admin(self.keystone)119 self.glance = u.authenticate_glance_admin(self.keystone)
105120
106 u.log.debug('openstack release: {}'.format(self._get_openstack_release()))121 def test_100_services(self):
107122 """Verify that the expected services are running on the
108 def test_services(self):123 corresponding service units."""
109 '''Verify that the expected services are running on the124 services = {
110 corresponding service units.'''125 self.mysql_sentry: ['mysql'],
111 commands = {126 self.keystone_sentry: ['keystone'],
112 self.mysql_sentry: ['status mysql'],127 self.glance_sentry: ['glance-api', 'glance-registry'],
113 self.keystone_sentry: ['status keystone'],128 self.rabbitmq_sentry: ['rabbitmq-server']
114 self.glance_sentry: ['status glance-api', 'status glance-registry'],
115 self.rabbitmq_sentry: ['sudo service rabbitmq-server status']
116 }129 }
117 u.log.debug('commands: {}'.format(commands))130
118 ret = u.validate_services(commands)131 ret = u.validate_services_by_name(services)
119 if ret:132 if ret:
120 amulet.raise_status(amulet.FAIL, msg=ret)133 amulet.raise_status(amulet.FAIL, msg=ret)
121134
122 def test_service_catalog(self):135 def test_102_service_catalog(self):
123 '''Verify that the service catalog endpoint data'''136 """Verify that the service catalog endpoint data is valid."""
124 endpoint_vol = {'adminURL': u.valid_url,137 u.log.debug('Checking keystone service catalog...')
125 'region': 'RegionOne',138 endpoint_check = {
126 'publicURL': u.valid_url,139 'adminURL': u.valid_url,
127 'internalURL': u.valid_url}140 'id': u.not_null,
128 endpoint_id = {'adminURL': u.valid_url,141 'region': 'RegionOne',
129 'region': 'RegionOne',142 'publicURL': u.valid_url,
130 'publicURL': u.valid_url,143 'internalURL': u.valid_url
131 'internalURL': u.valid_url}144 }
132 if self._get_openstack_release() >= self.trusty_icehouse:145 expected = {
133 endpoint_vol['id'] = u.not_null146 'image': [endpoint_check],
134 endpoint_id['id'] = u.not_null147 'identity': [endpoint_check]
135148 }
136 expected = {'image': [endpoint_id],
137 'identity': [endpoint_id]}
138 actual = self.keystone.service_catalog.get_endpoints()149 actual = self.keystone.service_catalog.get_endpoints()
139150
140 ret = u.validate_svc_catalog_endpoint_data(expected, actual)151 ret = u.validate_svc_catalog_endpoint_data(expected, actual)
141 if ret:152 if ret:
142 amulet.raise_status(amulet.FAIL, msg=ret)153 amulet.raise_status(amulet.FAIL, msg=ret)
143154
144 def test_mysql_glance_db_relation(self):155 def test_104_glance_endpoint(self):
145 '''Verify the mysql:glance shared-db relation data'''156 """Verify the glance endpoint data."""
157 u.log.debug('Checking glance api endpoint data...')
158 endpoints = self.keystone.endpoints.list()
159 admin_port = internal_port = public_port = '9292'
160 expected = {'id': u.not_null,
161 'region': 'RegionOne',
162 'adminurl': u.valid_url,
163 'internalurl': u.valid_url,
164 'publicurl': u.valid_url,
165 'service_id': u.not_null}
166 ret = u.validate_endpoint_data(endpoints, admin_port, internal_port,
167 public_port, expected)
168
169 if ret:
170 amulet.raise_status(amulet.FAIL,
171 msg='glance endpoint: {}'.format(ret))
172
173 def test_106_keystone_endpoint(self):
174 """Verify the keystone endpoint data."""
175 u.log.debug('Checking keystone api endpoint data...')
176 endpoints = self.keystone.endpoints.list()
177 admin_port = '35357'
178 internal_port = public_port = '5000'
179 expected = {'id': u.not_null,
180 'region': 'RegionOne',
181 'adminurl': u.valid_url,
182 'internalurl': u.valid_url,
183 'publicurl': u.valid_url,
184 'service_id': u.not_null}
185 ret = u.validate_endpoint_data(endpoints, admin_port, internal_port,
186 public_port, expected)
187 if ret:
188 amulet.raise_status(amulet.FAIL,
189 msg='keystone endpoint: {}'.format(ret))
190
191 def test_110_users(self):
192 """Verify expected users."""
193 u.log.debug('Checking keystone users...')
194 user0 = {'name': 'admin',
195 'enabled': True,
196 'tenantId': u.not_null,
197 'id': u.not_null,
198 'email': 'juju@localhost'}
199 user1 = {'name': 'glance',
200 'enabled': True,
201 'tenantId': u.not_null,
202 'id': u.not_null,
203 'email': 'juju@localhost'}
204 expected = [user0, user1]
205 actual = self.keystone.users.list()
206
207 ret = u.validate_user_data(expected, actual)
208 if ret:
209 amulet.raise_status(amulet.FAIL, msg=ret)
210
211 def test_200_mysql_glance_db_relation(self):
212 """Verify the mysql:glance shared-db relation data"""
213 u.log.debug('Checking mysql to glance shared-db relation data...')
146 unit = self.mysql_sentry214 unit = self.mysql_sentry
147 relation = ['shared-db', 'glance:shared-db']215 relation = ['shared-db', 'glance:shared-db']
148 expected = {216 expected = {
@@ -154,8 +222,9 @@
154 message = u.relation_error('mysql shared-db', ret)222 message = u.relation_error('mysql shared-db', ret)
155 amulet.raise_status(amulet.FAIL, msg=message)223 amulet.raise_status(amulet.FAIL, msg=message)
156224
157 def test_glance_mysql_db_relation(self):225 def test_201_glance_mysql_db_relation(self):
158 '''Verify the glance:mysql shared-db relation data'''226 """Verify the glance:mysql shared-db relation data"""
227 u.log.debug('Checking glance to mysql shared-db relation data...')
159 unit = self.glance_sentry228 unit = self.glance_sentry
160 relation = ['shared-db', 'mysql:shared-db']229 relation = ['shared-db', 'mysql:shared-db']
161 expected = {230 expected = {
@@ -169,8 +238,9 @@
169 message = u.relation_error('glance shared-db', ret)238 message = u.relation_error('glance shared-db', ret)
170 amulet.raise_status(amulet.FAIL, msg=message)239 amulet.raise_status(amulet.FAIL, msg=message)
171240
172 def test_keystone_glance_id_relation(self):241 def test_202_keystone_glance_id_relation(self):
173 '''Verify the keystone:glance identity-service relation data'''242 """Verify the keystone:glance identity-service relation data"""
243 u.log.debug('Checking keystone to glance id relation data...')
174 unit = self.keystone_sentry244 unit = self.keystone_sentry
175 relation = ['identity-service',245 relation = ['identity-service',
176 'glance:identity-service']246 'glance:identity-service']
@@ -193,8 +263,9 @@
193 message = u.relation_error('keystone identity-service', ret)263 message = u.relation_error('keystone identity-service', ret)
194 amulet.raise_status(amulet.FAIL, msg=message)264 amulet.raise_status(amulet.FAIL, msg=message)
195265
196 def test_glance_keystone_id_relation(self):266 def test_203_glance_keystone_id_relation(self):
197 '''Verify the glance:keystone identity-service relation data'''267 """Verify the glance:keystone identity-service relation data"""
268 u.log.debug('Checking glance to keystone relation data...')
198 unit = self.glance_sentry269 unit = self.glance_sentry
199 relation = ['identity-service',270 relation = ['identity-service',
200 'keystone:identity-service']271 'keystone:identity-service']
@@ -211,8 +282,9 @@
211 message = u.relation_error('glance identity-service', ret)282 message = u.relation_error('glance identity-service', ret)
212 amulet.raise_status(amulet.FAIL, msg=message)283 amulet.raise_status(amulet.FAIL, msg=message)
213284
214 def test_rabbitmq_glance_amqp_relation(self):285 def test_204_rabbitmq_glance_amqp_relation(self):
215 '''Verify the rabbitmq-server:glance amqp relation data'''286 """Verify the rabbitmq-server:glance amqp relation data"""
287 u.log.debug('Checking rmq to glance amqp relation data...')
216 unit = self.rabbitmq_sentry288 unit = self.rabbitmq_sentry
217 relation = ['amqp', 'glance:amqp']289 relation = ['amqp', 'glance:amqp']
218 expected = {290 expected = {
@@ -225,8 +297,9 @@
225 message = u.relation_error('rabbitmq amqp', ret)297 message = u.relation_error('rabbitmq amqp', ret)
226 amulet.raise_status(amulet.FAIL, msg=message)298 amulet.raise_status(amulet.FAIL, msg=message)
227299
228 def test_glance_rabbitmq_amqp_relation(self):300 def test_205_glance_rabbitmq_amqp_relation(self):
229 '''Verify the glance:rabbitmq-server amqp relation data'''301 """Verify the glance:rabbitmq-server amqp relation data"""
302 u.log.debug('Checking glance to rmq amqp relation data...')
230 unit = self.glance_sentry303 unit = self.glance_sentry
231 relation = ['amqp', 'rabbitmq-server:amqp']304 relation = ['amqp', 'rabbitmq-server:amqp']
232 expected = {305 expected = {
@@ -239,291 +312,180 @@
239 message = u.relation_error('glance amqp', ret)312 message = u.relation_error('glance amqp', ret)
240 amulet.raise_status(amulet.FAIL, msg=message)313 amulet.raise_status(amulet.FAIL, msg=message)
241314
242 def test_image_create_delete(self):315 def test_300_glance_api_default_config(self):
243 '''Create new cirros image in glance, verify, then delete it'''316 """Verify default section configs in glance-api.conf and
244317 compare some of the parameters to relation data."""
245 # Create a new image318 u.log.debug('Checking glance api config file...')
246 image_name = 'cirros-image-1'
247 image_new = u.create_cirros_image(self.glance, image_name)
248
249 # Confirm image is created and has status of 'active'
250 if not image_new:
251 message = 'glance image create failed'
252 amulet.raise_status(amulet.FAIL, msg=message)
253
254 # Verify new image name
255 images_list = list(self.glance.images.list())
256 if images_list[0].name != image_name:
257 message = 'glance image create failed or unexpected image name {}'.format(images_list[0].name)
258 amulet.raise_status(amulet.FAIL, msg=message)
259
260 # Delete the new image
261 u.log.debug('image count before delete: {}'.format(len(list(self.glance.images.list()))))
262 u.delete_image(self.glance, image_new)
263 u.log.debug('image count after delete: {}'.format(len(list(self.glance.images.list()))))
264
265 def test_glance_api_default_config(self):
266 '''Verify default section configs in glance-api.conf and
267 compare some of the parameters to relation data.'''
268 unit = self.glance_sentry319 unit = self.glance_sentry
320 unit_ks = self.keystone_sentry
269 rel_gl_mq = unit.relation('amqp', 'rabbitmq-server:amqp')321 rel_gl_mq = unit.relation('amqp', 'rabbitmq-server:amqp')
270 conf = '/etc/glance/glance-api.conf'322 rel_ks_gl = unit_ks.relation('identity-service',
271 expected = {'use_syslog': 'False',323 'glance:identity-service')
272 'default_store': 'file',324 rel_my_gl = self.mysql_sentry.relation('shared-db', 'glance:shared-db')
273 'filesystem_store_datadir': '/var/lib/glance/images/',325 db_uri = "mysql://{}:{}@{}/{}".format('glance', rel_my_gl['password'],
274 'rabbit_userid': rel_gl_mq['username'],326 rel_my_gl['db_host'], 'glance')
275 'log_file': '/var/log/glance/api.log',327 conf = '/etc/glance/glance-api.conf'
276 'debug': 'False',328 expected = {
277 'verbose': 'False'}329 'DEFAULT': {
278 section = 'DEFAULT'330 'debug': 'False',
279331 'verbose': 'False',
280 if self._get_openstack_release() <= self.precise_havana:332 'use_syslog': 'False',
281 # Defaults were different before icehouse333 'log_file': '/var/log/glance/api.log',
282 expected['debug'] = 'True'334 'default_store': 'file',
283 expected['verbose'] = 'True'335 'filesystem_store_datadir': '/var/lib/glance/images/',
284336 'rabbit_userid': rel_gl_mq['username'],
285 ret = u.validate_config_data(unit, conf, section, expected)337 'bind_host': '0.0.0.0',
286 if ret:338 'bind_port': '9282',
287 message = "glance-api default config error: {}".format(ret)339 'registry_host': '0.0.0.0',
288 amulet.raise_status(amulet.FAIL, msg=message)340 'registry_port': '9191',
289341 'registry_client_protocol': 'http',
290 def test_glance_api_auth_config(self):342 'delayed_delete': 'False',
291 '''Verify authtoken section config in glance-api.conf using343 'scrub_time': '43200',
292 glance/keystone relation data.'''344 'notification_driver': 'rabbit',
293 unit_gl = self.glance_sentry345 'filesystem_store_datadir': '/var/lib/glance/images/',
294 unit_ks = self.keystone_sentry346 'scrubber_datadir': '/var/lib/glance/scrubber',
295 rel_gl_mq = unit_gl.relation('amqp', 'rabbitmq-server:amqp')347 'image_cache_dir': '/var/lib/glance/image-cache/',
296 rel_ks_gl = unit_ks.relation('identity-service', 'glance:identity-service')348 'db_enforce_mysql_charset': 'False',
297 conf = '/etc/glance/glance-api.conf'349 'rabbit_userid': 'glance',
298 section = 'keystone_authtoken'350 'rabbit_virtual_host': 'openstack',
299351 'rabbit_password': u.not_null,
300 if self._get_openstack_release() > self.precise_havana:352 'rabbit_host': u.valid_ip
301 # No auth config exists in this file before icehouse353 },
302 expected = {'admin_user': 'glance',354 'keystone_authtoken': {
303 'admin_password': rel_ks_gl['service_password']}355 'admin_user': 'glance',
304356 'admin_password': rel_ks_gl['service_password'],
305 ret = u.validate_config_data(unit_gl, conf, section, expected)357 'auth_uri': u.valid_url,
358 'auth_host': u.valid_ip,
359 'auth_port': '35357',
360 'auth_protocol': 'http',
361 },
362 'database': {
363 'connection': db_uri,
364 'sql_idle_timeout': '3600'
365 }
366 }
367
368 for section, pairs in expected.iteritems():
369 ret = u.validate_config_data(unit, conf, section, pairs)
306 if ret:370 if ret:
307 message = "glance-api auth config error: {}".format(ret)371 message = "glance api config error: {}".format(ret)
308 amulet.raise_status(amulet.FAIL, msg=message)372 amulet.raise_status(amulet.FAIL, msg=message)
309373
310 def test_glance_api_paste_auth_config(self):374 def _get_filter_factory_expected_dict(self):
311 '''Verify authtoken section config in glance-api-paste.ini using375 """Return expected authtoken filter factory dict for OS release"""
312 glance/keystone relation data.'''376 if self._get_openstack_release() < self.vivid_kilo:
313 unit_gl = self.glance_sentry377 # Juno and earlier
314 unit_ks = self.keystone_sentry378 val = 'keystoneclient.middleware.auth_token:filter_factory'
315 rel_gl_mq = unit_gl.relation('amqp', 'rabbitmq-server:amqp')379 else:
316 rel_ks_gl = unit_ks.relation('identity-service', 'glance:identity-service')380 # Kilo and later
381 val = 'keystonemiddleware.auth_token: filter_factory'
382
383 return {'filter:authtoken': {'paste.filter_factory': val}}
384
385 def test_304_glance_api_paste_auth_config(self):
386 """Verify authtoken section config in glance-api-paste.ini using
387 glance/keystone relation data."""
388 u.log.debug('Checking glance api paste config file...')
389 unit = self.glance_sentry
317 conf = '/etc/glance/glance-api-paste.ini'390 conf = '/etc/glance/glance-api-paste.ini'
318 section = 'filter:authtoken'391 expected = self._get_filter_factory_expected_dict()
319392
320 if self._get_openstack_release() <= self.precise_havana:393 for section, pairs in expected.iteritems():
321 # No auth config exists in this file after havana394 ret = u.validate_config_data(unit, conf, section, pairs)
322 expected = {'admin_user': 'glance',
323 'admin_password': rel_ks_gl['service_password']}
324
325 ret = u.validate_config_data(unit_gl, conf, section, expected)
326 if ret:395 if ret:
327 message = "glance-api-paste auth config error: {}".format(ret)396 message = "glance api paste config error: {}".format(ret)
328 amulet.raise_status(amulet.FAIL, msg=message)397 amulet.raise_status(amulet.FAIL, msg=message)
329398
330 def test_glance_registry_paste_auth_config(self):399 def test_306_glance_registry_paste_auth_config(self):
331 '''Verify authtoken section config in glance-registry-paste.ini using400 """Verify authtoken section config in glance-registry-paste.ini using
332 glance/keystone relation data.'''401 glance/keystone relation data."""
333 unit_gl = self.glance_sentry402 u.log.debug('Checking glance registry paste config file...')
334 unit_ks = self.keystone_sentry403 unit = self.glance_sentry
335 rel_gl_mq = unit_gl.relation('amqp', 'rabbitmq-server:amqp')
336 rel_ks_gl = unit_ks.relation('identity-service', 'glance:identity-service')
337 conf = '/etc/glance/glance-registry-paste.ini'404 conf = '/etc/glance/glance-registry-paste.ini'
338 section = 'filter:authtoken'405 expected = self._get_filter_factory_expected_dict()
339406
340 if self._get_openstack_release() <= self.precise_havana:407 for section, pairs in expected.iteritems():
341 # No auth config exists in this file after havana408 ret = u.validate_config_data(unit, conf, section, pairs)
342 expected = {'admin_user': 'glance',
343 'admin_password': rel_ks_gl['service_password']}
344
345 ret = u.validate_config_data(unit_gl, conf, section, expected)
346 if ret:409 if ret:
347 message = "glance-registry-paste auth config error: {}".format(ret)410 message = "glance registry paste config error: {}".format(ret)
348 amulet.raise_status(amulet.FAIL, msg=message)411 amulet.raise_status(amulet.FAIL, msg=message)
349412
350 def test_glance_registry_default_config(self):413 def test_308_glance_registry_default_config(self):
351 '''Verify default section configs in glance-registry.conf'''414 """Verify configs in glance-registry.conf"""
415 u.log.debug('Checking glance registry config file...')
352 unit = self.glance_sentry416 unit = self.glance_sentry
353 conf = '/etc/glance/glance-registry.conf'
354 expected = {'use_syslog': 'False',
355 'log_file': '/var/log/glance/registry.log',
356 'debug': 'False',
357 'verbose': 'False'}
358 section = 'DEFAULT'
359
360 if self._get_openstack_release() <= self.precise_havana:
361 # Defaults were different before icehouse
362 expected['debug'] = 'True'
363 expected['verbose'] = 'True'
364
365 ret = u.validate_config_data(unit, conf, section, expected)
366 if ret:
367 message = "glance-registry default config error: {}".format(ret)
368 amulet.raise_status(amulet.FAIL, msg=message)
369
370 def test_glance_registry_auth_config(self):
371 '''Verify authtoken section config in glance-registry.conf
372 using glance/keystone relation data.'''
373 unit_gl = self.glance_sentry
374 unit_ks = self.keystone_sentry417 unit_ks = self.keystone_sentry
375 rel_gl_mq = unit_gl.relation('amqp', 'rabbitmq-server:amqp')418 rel_ks_gl = unit_ks.relation('identity-service',
376 rel_ks_gl = unit_ks.relation('identity-service', 'glance:identity-service')419 'glance:identity-service')
420 rel_my_gl = self.mysql_sentry.relation('shared-db', 'glance:shared-db')
421 db_uri = "mysql://{}:{}@{}/{}".format('glance', rel_my_gl['password'],
422 rel_my_gl['db_host'], 'glance')
377 conf = '/etc/glance/glance-registry.conf'423 conf = '/etc/glance/glance-registry.conf'
378 section = 'keystone_authtoken'424
379425 expected = {
380 if self._get_openstack_release() > self.precise_havana:426 'DEFAULT': {
381 # No auth config exists in this file before icehouse427 'use_syslog': 'False',
382 expected = {'admin_user': 'glance',428 'log_file': '/var/log/glance/registry.log',
383 'admin_password': rel_ks_gl['service_password']}429 'debug': 'False',
384430 'verbose': 'False',
385 ret = u.validate_config_data(unit_gl, conf, section, expected)431 'bind_host': '0.0.0.0',
432 'bind_port': '9191'
433 },
434 'database': {
435 'connection': db_uri,
436 'sql_idle_timeout': '3600'
437 },
438 'keystone_authtoken': {
439 'admin_user': 'glance',
440 'admin_password': rel_ks_gl['service_password'],
441 'auth_uri': u.valid_url,
442 'auth_host': u.valid_ip,
443 'auth_port': '35357',
444 'auth_protocol': 'http',
445 },
446 }
447
448 for section, pairs in expected.iteritems():
449 ret = u.validate_config_data(unit, conf, section, pairs)
386 if ret:450 if ret:
387 message = "glance-registry keystone_authtoken config error: {}".format(ret)451 message = "glance registry paste config error: {}".format(ret)
388 amulet.raise_status(amulet.FAIL, msg=message)452 amulet.raise_status(amulet.FAIL, msg=message)
389453
390 def test_glance_api_database_config(self):454 def test_410_glance_image_create_delete(self):
391 '''Verify database config in glance-api.conf and455 """Create new cirros image in glance, verify, then delete it."""
392 compare with a db uri constructed from relation data.'''456 u.log.debug('Creating, checking and deleting glance image...')
393 unit = self.glance_sentry457 img_new = u.create_cirros_image(self.glance, "cirros-image-1")
394 conf = '/etc/glance/glance-api.conf'458 img_id = img_new.id
395 relation = self.mysql_sentry.relation('shared-db', 'glance:shared-db')459 u.delete_resource(self.glance.images, img_id, msg="glance image")
396 db_uri = "mysql://{}:{}@{}/{}".format('glance', relation['password'],460
397 relation['db_host'], 'glance')461 def test_900_glance_restart_on_config_change(self):
398 expected = {'connection': db_uri, 'sql_idle_timeout': '3600'}462 """Verify that the specified services are restarted when the config
399 section = 'database'463 is changed."""
400464 sentry = self.glance_sentry
401 if self._get_openstack_release() <= self.precise_havana:465 juju_service = 'glance'
402 # Section and directive for this config changed in icehouse466
403 expected = {'sql_connection': db_uri, 'sql_idle_timeout': '3600'}467 # Expected default and alternate values
404 section = 'DEFAULT'468 set_default = {'use-syslog': 'False'}
405469 set_alternate = {'use-syslog': 'True'}
406 ret = u.validate_config_data(unit, conf, section, expected) 470
407 if ret:471 # Config file affected by juju set config change
408 message = "glance db config error: {}".format(ret)472 conf_file = '/etc/glance/glance-api.conf'
409 amulet.raise_status(amulet.FAIL, msg=message)473
410474 # Services which are expected to restart upon config change
411 def test_glance_registry_database_config(self):475 services = ['glance-api', 'glance-registry']
412 '''Verify database config in glance-registry.conf and476
413 compare with a db uri constructed from relation data.'''477 # Make config change, check for service restarts
414 unit = self.glance_sentry478 u.log.debug('Making config change on {}...'.format(juju_service))
415 conf = '/etc/glance/glance-registry.conf'479 self.d.configure(juju_service, set_alternate)
416 relation = self.mysql_sentry.relation('shared-db', 'glance:shared-db')480
417 db_uri = "mysql://{}:{}@{}/{}".format('glance', relation['password'],481 sleep_time = 30
418 relation['db_host'], 'glance')482 for s in services:
419 expected = {'connection': db_uri, 'sql_idle_timeout': '3600'}483 u.log.debug("Checking that service restarted: {}".format(s))
420 section = 'database'484 if not u.service_restarted(sentry, s,
421485 conf_file, sleep_time=sleep_time):
422 if self._get_openstack_release() <= self.precise_havana:486 self.d.configure(juju_service, set_default)
423 # Section and directive for this config changed in icehouse487 msg = "service {} didn't restart after config change".format(s)
424 expected = {'sql_connection': db_uri, 'sql_idle_timeout': '3600'}488 amulet.raise_status(amulet.FAIL, msg=msg)
425 section = 'DEFAULT'489 sleep_time = 0
426490
427 ret = u.validate_config_data(unit, conf, section, expected)491 self.d.configure(juju_service, set_default)
428 if ret:
429 message = "glance db config error: {}".format(ret)
430 amulet.raise_status(amulet.FAIL, msg=message)
431
432 def test_glance_endpoint(self):
433 '''Verify the glance endpoint data.'''
434 endpoints = self.keystone.endpoints.list()
435 admin_port = internal_port = public_port = '9292'
436 expected = {'id': u.not_null,
437 'region': 'RegionOne',
438 'adminurl': u.valid_url,
439 'internalurl': u.valid_url,
440 'publicurl': u.valid_url,
441 'service_id': u.not_null}
442 ret = u.validate_endpoint_data(endpoints, admin_port, internal_port,
443 public_port, expected)
444
445 if ret:
446 amulet.raise_status(amulet.FAIL,
447 msg='glance endpoint: {}'.format(ret))
448
449 def test_keystone_endpoint(self):
450 '''Verify the keystone endpoint data.'''
451 endpoints = self.keystone.endpoints.list()
452 admin_port = '35357'
453 internal_port = public_port = '5000'
454 expected = {'id': u.not_null,
455 'region': 'RegionOne',
456 'adminurl': u.valid_url,
457 'internalurl': u.valid_url,
458 'publicurl': u.valid_url,
459 'service_id': u.not_null}
460 ret = u.validate_endpoint_data(endpoints, admin_port, internal_port,
461 public_port, expected)
462 if ret:
463 amulet.raise_status(amulet.FAIL,
464 msg='keystone endpoint: {}'.format(ret))
465
466 def _change_config(self):
467 if self._get_openstack_release() > self.precise_havana:
468 self.d.configure('glance', {'debug': 'True'})
469 else:
470 self.d.configure('glance', {'debug': 'False'})
471
472 def _restore_config(self):
473 if self._get_openstack_release() > self.precise_havana:
474 self.d.configure('glance', {'debug': 'False'})
475 else:
476 self.d.configure('glance', {'debug': 'True'})
477
478 def test_z_glance_restart_on_config_change(self):
479 '''Verify that glance is restarted when the config is changed.
480
481 Note(coreycb): The method name with the _z_ is a little odd
482 but it forces the test to run last. It just makes things
483 easier because restarting services requires re-authorization.
484 '''
485 if self._get_openstack_release() <= self.precise_havana:
486 # /!\ NOTE(beisner): Glance charm before Icehouse doesn't respond
487 # to attempted config changes via juju / juju set.
488 # https://bugs.launchpad.net/charms/+source/glance/+bug/1340307
489 u.log.error('NOTE(beisner): skipping glance restart on config ' +
490 'change check due to bug 1340307.')
491 return
492
493 # Make config change to trigger a service restart
494 self._change_config()
495
496 if not u.service_restarted(self.glance_sentry, 'glance-api',
497 '/etc/glance/glance-api.conf'):
498 self._restore_config()
499 message = "glance service didn't restart after config change"
500 amulet.raise_status(amulet.FAIL, msg=message)
501
502 if not u.service_restarted(self.glance_sentry, 'glance-registry',
503 '/etc/glance/glance-registry.conf',
504 sleep_time=0):
505 self._restore_config()
506 message = "glance service didn't restart after config change"
507 amulet.raise_status(amulet.FAIL, msg=message)
508
509 # Return to original config
510 self._restore_config()
511
512 def test_users(self):
513 '''Verify expected users.'''
514 user0 = {'name': 'glance',
515 'enabled': True,
516 'tenantId': u.not_null,
517 'id': u.not_null,
518 'email': 'juju@localhost'}
519 user1 = {'name': 'admin',
520 'enabled': True,
521 'tenantId': u.not_null,
522 'id': u.not_null,
523 'email': 'juju@localhost'}
524 expected = [user0, user1]
525 actual = self.keystone.users.list()
526
527 ret = u.validate_user_data(expected, actual)
528 if ret:
529 amulet.raise_status(amulet.FAIL, msg=ret)
530492
=== modified file 'tests/charmhelpers/contrib/amulet/utils.py'
--- tests/charmhelpers/contrib/amulet/utils.py 2015-04-23 14:52:07 +0000
+++ tests/charmhelpers/contrib/amulet/utils.py 2015-06-30 20:18:04 +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):
@@ -112,10 +185,23 @@
112 for k in expected.keys():185 for k in expected.keys():
113 if not config.has_option(section, k):186 if not config.has_option(section, k):
114 return "section [{}] is missing option {}".format(section, k)187 return "section [{}] is missing option {}".format(section, k)
115 if config.get(section, k) != expected[k]:188
116 return "section [{}] {}:{} != expected {}:{}".format(189 actual = config.get(section, k)
117 section, k, config.get(section, k), k, expected[k])190 v = expected[k]
118 return None191 if (isinstance(v, six.string_types) or
192 isinstance(v, bool) or
193 isinstance(v, six.integer_types)):
194 # handle explicit values
195 if actual != v:
196 return "section [{}] {}:{} != expected {}:{}".format(
197 section, k, actual, k, expected[k])
198 else:
199 # handle not_null, valid_ip boolean comparison methods, etc.
200 if v(actual):
201 return None
202 else:
203 return "section [{}] {}:{} != expected {}:{}".format(
204 section, k, actual, k, expected[k])
119205
120 def _validate_dict_data(self, expected, actual):206 def _validate_dict_data(self, expected, actual):
121 """Validate dictionary data.207 """Validate dictionary data.
@@ -321,3 +407,135 @@
321407
322 def endpoint_error(self, name, data):408 def endpoint_error(self, name, data):
323 return 'unexpected endpoint data in {} - {}'.format(name, data)409 return 'unexpected endpoint data in {} - {}'.format(name, data)
410
411 def get_ubuntu_releases(self):
412 """Return a list of all Ubuntu releases in order of release."""
413 _d = distro_info.UbuntuDistroInfo()
414 _release_list = _d.all
415 self.log.debug('Ubuntu release list: {}'.format(_release_list))
416 return _release_list
417
418 def file_to_url(self, file_rel_path):
419 """Convert a relative file path to a file URL."""
420 _abs_path = os.path.abspath(file_rel_path)
421 return urlparse.urlparse(_abs_path, scheme='file').geturl()
422
423 def check_commands_on_units(self, commands, sentry_units):
424 """Check that all commands in a list exit zero on all
425 sentry units in a list.
426
427 :param commands: list of bash commands
428 :param sentry_units: list of sentry unit pointers
429 :returns: None if successful; Failure message otherwise
430 """
431 self.log.debug('Checking exit codes for {} commands on {} '
432 'sentry units...'.format(len(commands),
433 len(sentry_units)))
434 for sentry_unit in sentry_units:
435 for cmd in commands:
436 output, code = sentry_unit.run(cmd)
437 if code == 0:
438 msg = ('{} `{}` returned {} '
439 '(OK)'.format(sentry_unit.info['unit_name'],
440 cmd, code))
441 self.log.debug(msg)
442 else:
443 msg = ('{} `{}` returned {} '
444 '{}'.format(sentry_unit.info['unit_name'],
445 cmd, code, output))
446 return msg
447 return None
448
449 def get_process_id_list(self, sentry_unit, process_name):
450 """Get a list of process ID(s) from a single sentry juju unit
451 for a single process name.
452
453 :param sentry_unit: Pointer to amulet sentry instance (juju unit)
454 :param process_name: Process name
455 :returns: List of process IDs
456 """
457 cmd = 'pidof {}'.format(process_name)
458 output, code = sentry_unit.run(cmd)
459 if code != 0:
460 msg = ('{} `{}` returned {} '
461 '{}'.format(sentry_unit.info['unit_name'],
462 cmd, code, output))
463 raise RuntimeError(msg)
464 return str(output).split()
465
466 def get_unit_process_ids(self, unit_processes):
467 """Construct a dict containing unit sentries, process names, and
468 process IDs."""
469 pid_dict = {}
470 for sentry_unit, process_list in unit_processes.iteritems():
471 pid_dict[sentry_unit] = {}
472 for process in process_list:
473 pids = self.get_process_id_list(sentry_unit, process)
474 pid_dict[sentry_unit].update({process: pids})
475 return pid_dict
476
477 def validate_unit_process_ids(self, expected, actual):
478 """Validate process id quantities for services on units."""
479 self.log.debug('Checking units for running processes...')
480 self.log.debug('Expected PIDs: {}'.format(expected))
481 self.log.debug('Actual PIDs: {}'.format(actual))
482
483 if len(actual) != len(expected):
484 msg = ('Unit count mismatch. expected, actual: {}, '
485 '{} '.format(len(expected), len(actual)))
486 return msg
487
488 for (e_sentry, e_proc_names) in expected.iteritems():
489 e_sentry_name = e_sentry.info['unit_name']
490 if e_sentry in actual.keys():
491 a_proc_names = actual[e_sentry]
492 else:
493 msg = ('Expected sentry ({}) not found in actual dict data.'
494 '{}'.format(e_sentry_name, e_sentry))
495 return msg
496
497 if len(e_proc_names.keys()) != len(a_proc_names.keys()):
498 msg = ('Process name count mismatch. expected, actual: {}, '
499 '{}'.format(len(expected), len(actual)))
500 return msg
501
502 for (e_proc_name, e_pids_length), (a_proc_name, a_pids) in \
503 zip(e_proc_names.items(), a_proc_names.items()):
504 if e_proc_name != a_proc_name:
505 msg = ('Process name mismatch. expected, actual: {}, '
506 '{}'.format(e_proc_name, a_proc_name))
507 return msg
508
509 a_pids_length = len(a_pids)
510 if e_pids_length != a_pids_length:
511 msg = ('PID count mismatch. {} ({}) expected, actual: {}, '
512 '{} ({})'.format(e_sentry_name,
513 e_proc_name,
514 e_pids_length,
515 a_pids_length,
516 a_pids))
517 return msg
518 else:
519 msg = ('PID check OK: {} {} {}: '
520 '{}'.format(e_sentry_name,
521 e_proc_name,
522 e_pids_length,
523 a_pids))
524 self.log.debug(msg)
525 return None
526
527 def validate_list_of_identical_dicts(self, list_of_dicts):
528 """Check that all dicts within a list are identical."""
529 hashes = []
530 for _dict in list_of_dicts:
531 hashes.append(hash(frozenset(_dict.items())))
532
533 self.log.debug('Hashes: {}'.format(hashes))
534 if len(set(hashes)) == 1:
535 msg = 'Dicts within list are identical'
536 self.log.debug(msg)
537 else:
538 msg = 'Dicts within list are not identical'
539 return msg
540
541 return None
324542
=== modified file 'tests/charmhelpers/contrib/openstack/amulet/deployment.py'
--- tests/charmhelpers/contrib/openstack/amulet/deployment.py 2015-04-23 14:52:07 +0000
+++ tests/charmhelpers/contrib/openstack/amulet/deployment.py 2015-06-30 20:18:04 +0000
@@ -79,9 +79,9 @@
79 services.append(this_service)79 services.append(this_service)
80 use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph',80 use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph',
81 'ceph-osd', 'ceph-radosgw']81 'ceph-osd', 'ceph-radosgw']
82 # Openstack subordinate charms do not expose an origin option as that82 # Most OpenStack subordinate charms do not expose an origin option
83 # is controlled by the principle83 # as that is controlled by the principle.
84 ignore = ['neutron-openvswitch']84 ignore = ['cinder-ceph', 'hacluster', 'neutron-openvswitch']
8585
86 if self.openstack:86 if self.openstack:
87 for svc in services:87 for svc in services:
@@ -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,9 +141,42 @@
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]
144 return os_origin.split('%s-' % self.series)[1].split('/')[0]148 return os_origin.split('%s-' % self.series)[1].split('/')[0]
145 else:149 else:
146 return releases[self.series]150 return releases[self.series]
151
152 def get_ceph_expected_pools(self, radosgw=False):
153 """Return a list of expected ceph pools based on Ubuntu-OpenStack
154 release and whether ceph radosgw is flagged as present or not."""
155
156 if self._get_openstack_release() >= self.trusty_kilo:
157 # Kilo or later
158 pools = [
159 'rbd',
160 'cinder',
161 'glance'
162 ]
163 else:
164 # Juno or earlier
165 pools = [
166 'data',
167 'metadata',
168 'rbd',
169 'cinder',
170 'glance'
171 ]
172
173 if radosgw:
174 pools.extend([
175 '.rgw.root',
176 '.rgw.control',
177 '.rgw',
178 '.rgw.gc',
179 '.users.uid'
180 ])
181
182 return pools
147183
=== modified file 'tests/charmhelpers/contrib/openstack/amulet/utils.py'
--- tests/charmhelpers/contrib/openstack/amulet/utils.py 2015-03-20 17:15:02 +0000
+++ tests/charmhelpers/contrib/openstack/amulet/utils.py 2015-06-30 20:18:04 +0000
@@ -14,16 +14,19 @@
14# You should have received a copy of the GNU Lesser General Public License14# You should have received a copy of the GNU Lesser General Public License
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 json
17import logging18import logging
18import os19import os
20import six
19import time21import time
20import urllib22import urllib
2123
24import cinderclient.v1.client as cinder_client
22import glanceclient.v1.client as glance_client25import glanceclient.v1.client as glance_client
26import heatclient.v1.client as heat_client
23import keystoneclient.v2_0 as keystone_client27import keystoneclient.v2_0 as keystone_client
24import novaclient.v1_1.client as nova_client28import novaclient.v1_1.client as nova_client
2529import swiftclient
26import six
2730
28from charmhelpers.contrib.amulet.utils import (31from charmhelpers.contrib.amulet.utils import (
29 AmuletUtils32 AmuletUtils
@@ -37,7 +40,7 @@
37 """OpenStack amulet utilities.40 """OpenStack amulet utilities.
3841
39 This class inherits from AmuletUtils and has additional support42 This class inherits from AmuletUtils and has additional support
40 that is specifically for use by OpenStack charms.43 that is specifically for use by OpenStack charm tests.
41 """44 """
4245
43 def __init__(self, log_level=ERROR):46 def __init__(self, log_level=ERROR):
@@ -51,6 +54,8 @@
51 Validate actual endpoint data vs expected endpoint data. The ports54 Validate actual endpoint data vs expected endpoint data. The ports
52 are used to find the matching endpoint.55 are used to find the matching endpoint.
53 """56 """
57 self.log.debug('Validating endpoint data...')
58 self.log.debug('actual: {}'.format(repr(endpoints)))
54 found = False59 found = False
55 for ep in endpoints:60 for ep in endpoints:
56 self.log.debug('endpoint: {}'.format(repr(ep)))61 self.log.debug('endpoint: {}'.format(repr(ep)))
@@ -77,6 +82,7 @@
77 Validate a list of actual service catalog endpoints vs a list of82 Validate a list of actual service catalog endpoints vs a list of
78 expected service catalog endpoints.83 expected service catalog endpoints.
79 """84 """
85 self.log.debug('Validating service catalog endpoint data...')
80 self.log.debug('actual: {}'.format(repr(actual)))86 self.log.debug('actual: {}'.format(repr(actual)))
81 for k, v in six.iteritems(expected):87 for k, v in six.iteritems(expected):
82 if k in actual:88 if k in actual:
@@ -93,6 +99,7 @@
93 Validate a list of actual tenant data vs list of expected tenant99 Validate a list of actual tenant data vs list of expected tenant
94 data.100 data.
95 """101 """
102 self.log.debug('Validating tenant data...')
96 self.log.debug('actual: {}'.format(repr(actual)))103 self.log.debug('actual: {}'.format(repr(actual)))
97 for e in expected:104 for e in expected:
98 found = False105 found = False
@@ -114,6 +121,7 @@
114 Validate a list of actual role data vs a list of expected role121 Validate a list of actual role data vs a list of expected role
115 data.122 data.
116 """123 """
124 self.log.debug('Validating role data...')
117 self.log.debug('actual: {}'.format(repr(actual)))125 self.log.debug('actual: {}'.format(repr(actual)))
118 for e in expected:126 for e in expected:
119 found = False127 found = False
@@ -134,6 +142,7 @@
134 Validate a list of actual user data vs a list of expected user142 Validate a list of actual user data vs a list of expected user
135 data.143 data.
136 """144 """
145 self.log.debug('Validating user data...')
137 self.log.debug('actual: {}'.format(repr(actual)))146 self.log.debug('actual: {}'.format(repr(actual)))
138 for e in expected:147 for e in expected:
139 found = False148 found = False
@@ -155,17 +164,29 @@
155164
156 Validate a list of actual flavors vs a list of expected flavors.165 Validate a list of actual flavors vs a list of expected flavors.
157 """166 """
167 self.log.debug('Validating flavor data...')
158 self.log.debug('actual: {}'.format(repr(actual)))168 self.log.debug('actual: {}'.format(repr(actual)))
159 act = [a.name for a in actual]169 act = [a.name for a in actual]
160 return self._validate_list_data(expected, act)170 return self._validate_list_data(expected, act)
161171
162 def tenant_exists(self, keystone, tenant):172 def tenant_exists(self, keystone, tenant):
163 """Return True if tenant exists."""173 """Return True if tenant exists."""
174 self.log.debug('Checking if tenant exists ({})...'.format(tenant))
164 return tenant in [t.name for t in keystone.tenants.list()]175 return tenant in [t.name for t in keystone.tenants.list()]
165176
177 def authenticate_cinder_admin(self, keystone_sentry, username,
178 password, tenant):
179 """Authenticates admin user with cinder."""
180 service_ip = \
181 keystone_sentry.relation('shared-db',
182 'mysql:shared-db')['private-address']
183 ept = "http://{}:5000/v2.0".format(service_ip.strip().decode('utf-8'))
184 return cinder_client.Client(username, password, tenant, ept)
185
166 def authenticate_keystone_admin(self, keystone_sentry, user, password,186 def authenticate_keystone_admin(self, keystone_sentry, user, password,
167 tenant):187 tenant):
168 """Authenticates admin user with the keystone admin endpoint."""188 """Authenticates admin user with the keystone admin endpoint."""
189 self.log.debug('Authenticating keystone admin...')
169 unit = keystone_sentry190 unit = keystone_sentry
170 service_ip = unit.relation('shared-db',191 service_ip = unit.relation('shared-db',
171 'mysql:shared-db')['private-address']192 'mysql:shared-db')['private-address']
@@ -175,6 +196,7 @@
175196
176 def authenticate_keystone_user(self, keystone, user, password, tenant):197 def authenticate_keystone_user(self, keystone, user, password, tenant):
177 """Authenticates a regular user with the keystone public endpoint."""198 """Authenticates a regular user with the keystone public endpoint."""
199 self.log.debug('Authenticating keystone user ({})...'.format(user))
178 ep = keystone.service_catalog.url_for(service_type='identity',200 ep = keystone.service_catalog.url_for(service_type='identity',
179 endpoint_type='publicURL')201 endpoint_type='publicURL')
180 return keystone_client.Client(username=user, password=password,202 return keystone_client.Client(username=user, password=password,
@@ -182,19 +204,49 @@
182204
183 def authenticate_glance_admin(self, keystone):205 def authenticate_glance_admin(self, keystone):
184 """Authenticates admin user with glance."""206 """Authenticates admin user with glance."""
207 self.log.debug('Authenticating glance admin...')
185 ep = keystone.service_catalog.url_for(service_type='image',208 ep = keystone.service_catalog.url_for(service_type='image',
186 endpoint_type='adminURL')209 endpoint_type='adminURL')
187 return glance_client.Client(ep, token=keystone.auth_token)210 return glance_client.Client(ep, token=keystone.auth_token)
188211
212 def authenticate_heat_admin(self, keystone):
213 """Authenticates the admin user with heat."""
214 self.log.debug('Authenticating heat admin...')
215 ep = keystone.service_catalog.url_for(service_type='orchestration',
216 endpoint_type='publicURL')
217 return heat_client.Client(endpoint=ep, token=keystone.auth_token)
218
189 def authenticate_nova_user(self, keystone, user, password, tenant):219 def authenticate_nova_user(self, keystone, user, password, tenant):
190 """Authenticates a regular user with nova-api."""220 """Authenticates a regular user with nova-api."""
221 self.log.debug('Authenticating nova user ({})...'.format(user))
191 ep = keystone.service_catalog.url_for(service_type='identity',222 ep = keystone.service_catalog.url_for(service_type='identity',
192 endpoint_type='publicURL')223 endpoint_type='publicURL')
193 return nova_client.Client(username=user, api_key=password,224 return nova_client.Client(username=user, api_key=password,
194 project_id=tenant, auth_url=ep)225 project_id=tenant, auth_url=ep)
195226
227 def authenticate_swift_user(self, keystone, user, password, tenant):
228 """Authenticates a regular user with swift api."""
229 self.log.debug('Authenticating swift user ({})...'.format(user))
230 ep = keystone.service_catalog.url_for(service_type='identity',
231 endpoint_type='publicURL')
232 return swiftclient.Connection(authurl=ep,
233 user=user,
234 key=password,
235 tenant_name=tenant,
236 auth_version='2.0')
237
196 def create_cirros_image(self, glance, image_name):238 def create_cirros_image(self, glance, image_name):
197 """Download the latest cirros image and upload it to glance."""239 """Download the latest cirros image and upload it to glance,
240 validate and return a resource pointer.
241
242 :param glance: pointer to authenticated glance connection
243 :param image_name: display name for new image
244 :returns: glance image pointer
245 """
246 self.log.debug('Creating glance cirros image '
247 '({})...'.format(image_name))
248
249 # Download cirros image
198 http_proxy = os.getenv('AMULET_HTTP_PROXY')250 http_proxy = os.getenv('AMULET_HTTP_PROXY')
199 self.log.debug('AMULET_HTTP_PROXY: {}'.format(http_proxy))251 self.log.debug('AMULET_HTTP_PROXY: {}'.format(http_proxy))
200 if http_proxy:252 if http_proxy:
@@ -203,57 +255,67 @@
203 else:255 else:
204 opener = urllib.FancyURLopener()256 opener = urllib.FancyURLopener()
205257
206 f = opener.open("http://download.cirros-cloud.net/version/released")258 f = opener.open('http://download.cirros-cloud.net/version/released')
207 version = f.read().strip()259 version = f.read().strip()
208 cirros_img = "cirros-{}-x86_64-disk.img".format(version)260 cirros_img = 'cirros-{}-x86_64-disk.img'.format(version)
209 local_path = os.path.join('tests', cirros_img)261 local_path = os.path.join('tests', cirros_img)
210262
211 if not os.path.exists(local_path):263 if not os.path.exists(local_path):
212 cirros_url = "http://{}/{}/{}".format("download.cirros-cloud.net",264 cirros_url = 'http://{}/{}/{}'.format('download.cirros-cloud.net',
213 version, cirros_img)265 version, cirros_img)
214 opener.retrieve(cirros_url, local_path)266 opener.retrieve(cirros_url, local_path)
215 f.close()267 f.close()
216268
269 # Create glance image
217 with open(local_path) as f:270 with open(local_path) as f:
218 image = glance.images.create(name=image_name, is_public=True,271 image = glance.images.create(name=image_name, is_public=True,
219 disk_format='qcow2',272 disk_format='qcow2',
220 container_format='bare', data=f)273 container_format='bare', data=f)
221 count = 1274
222 status = image.status275 # Wait for image to reach active status
223 while status != 'active' and count < 10:276 img_id = image.id
224 time.sleep(3)277 ret = self.resource_reaches_status(glance.images, img_id,
225 image = glance.images.get(image.id)278 expected_stat='active',
226 status = image.status279 msg='Image status wait')
227 self.log.debug('image status: {}'.format(status))280 if not ret:
228 count += 1281 msg = 'Glance image failed to reach expected state.'
229282 raise RuntimeError(msg)
230 if status != 'active':283
231 self.log.error('image creation timed out')284 # Re-validate new image
232 return None285 self.log.debug('Validating image attributes...')
286 val_img_name = glance.images.get(img_id).name
287 val_img_stat = glance.images.get(img_id).status
288 val_img_pub = glance.images.get(img_id).is_public
289 val_img_cfmt = glance.images.get(img_id).container_format
290 val_img_dfmt = glance.images.get(img_id).disk_format
291 msg_attr = ('Image attributes - name:{} public:{} id:{} stat:{} '
292 'container fmt:{} disk fmt:{}'.format(
293 val_img_name, val_img_pub, img_id,
294 val_img_stat, val_img_cfmt, val_img_dfmt))
295
296 if val_img_name == image_name and val_img_stat == 'active' \
297 and val_img_pub is True and val_img_cfmt == 'bare' \
298 and val_img_dfmt == 'qcow2':
299 self.log.debug(msg_attr)
300 else:
301 msg = ('Volume validation failed, {}'.format(msg_attr))
302 raise RuntimeError(msg)
233303
234 return image304 return image
235305
236 def delete_image(self, glance, image):306 def delete_image(self, glance, image):
237 """Delete the specified image."""307 """Delete the specified image."""
238 num_before = len(list(glance.images.list()))308
239 glance.images.delete(image)309 # /!\ DEPRECATION WARNING
240310 self.log.warn('/!\\ DEPRECATION WARNING: use '
241 count = 1311 'delete_resource instead of delete_image.')
242 num_after = len(list(glance.images.list()))312 self.log.debug('Deleting glance image ({})...'.format(image))
243 while num_after != (num_before - 1) and count < 10:313 return self.delete_resource(glance.images, image, msg='glance image')
244 time.sleep(3)
245 num_after = len(list(glance.images.list()))
246 self.log.debug('number of images: {}'.format(num_after))
247 count += 1
248
249 if num_after != (num_before - 1):
250 self.log.error('image deletion timed out')
251 return False
252
253 return True
254314
255 def create_instance(self, nova, image_name, instance_name, flavor):315 def create_instance(self, nova, image_name, instance_name, flavor):
256 """Create the specified instance."""316 """Create the specified instance."""
317 self.log.debug('Creating instance '
318 '({}|{}|{})'.format(instance_name, image_name, flavor))
257 image = nova.images.find(name=image_name)319 image = nova.images.find(name=image_name)
258 flavor = nova.flavors.find(name=flavor)320 flavor = nova.flavors.find(name=flavor)
259 instance = nova.servers.create(name=instance_name, image=image,321 instance = nova.servers.create(name=instance_name, image=image,
@@ -276,19 +338,264 @@
276338
277 def delete_instance(self, nova, instance):339 def delete_instance(self, nova, instance):
278 """Delete the specified instance."""340 """Delete the specified instance."""
279 num_before = len(list(nova.servers.list()))341
280 nova.servers.delete(instance)342 # /!\ DEPRECATION WARNING
281343 self.log.warn('/!\\ DEPRECATION WARNING: use '
282 count = 1344 'delete_resource instead of delete_instance.')
283 num_after = len(list(nova.servers.list()))345 self.log.debug('Deleting instance ({})...'.format(instance))
284 while num_after != (num_before - 1) and count < 10:346 return self.delete_resource(nova.servers, instance,
285 time.sleep(3)347 msg='nova instance')
286 num_after = len(list(nova.servers.list()))348
287 self.log.debug('number of instances: {}'.format(num_after))349 def create_or_get_keypair(self, nova, keypair_name="testkey"):
288 count += 1350 """Create a new keypair, or return pointer if it already exists."""
289351 try:
290 if num_after != (num_before - 1):352 _keypair = nova.keypairs.get(keypair_name)
291 self.log.error('instance deletion timed out')353 self.log.debug('Keypair ({}) already exists, '
292 return False354 'using it.'.format(keypair_name))
293355 return _keypair
294 return True356 except:
357 self.log.debug('Keypair ({}) does not exist, '
358 'creating it.'.format(keypair_name))
359
360 _keypair = nova.keypairs.create(name=keypair_name)
361 return _keypair
362
363 def create_cinder_volume(self, cinder, vol_name="demo-vol", vol_size=1,
364 img_id=None, src_vol_id=None, snap_id=None):
365 """Create cinder volume, optionally from a glance image, or
366 optionally as a clone of an existing volume, or optionally
367 from a snapshot. Wait for the new volume status to reach
368 the expected status, validate and return a resource pointer.
369
370 :param vol_name: cinder volume display name
371 :param vol_size: size in gigabytes
372 :param img_id: optional glance image id
373 :param src_vol_id: optional source volume id to clone
374 :param snap_id: optional snapshot id to use
375 :returns: cinder volume pointer
376 """
377 # Handle parameter input
378 if img_id and not src_vol_id and not snap_id:
379 self.log.debug('Creating cinder volume from glance image '
380 '({})...'.format(img_id))
381 bootable = 'true'
382 elif src_vol_id and not img_id and not snap_id:
383 self.log.debug('Cloning cinder volume...')
384 bootable = cinder.volumes.get(src_vol_id).bootable
385 elif snap_id and not src_vol_id and not img_id:
386 self.log.debug('Creating cinder volume from snapshot...')
387 snap = cinder.volume_snapshots.find(id=snap_id)
388 vol_size = snap.size
389 snap_vol_id = cinder.volume_snapshots.get(snap_id).volume_id
390 bootable = cinder.volumes.get(snap_vol_id).bootable
391 elif not img_id and not src_vol_id and not snap_id:
392 self.log.debug('Creating cinder volume...')
393 bootable = 'false'
394 else:
395 msg = ('Invalid method use - name:{} size:{} img_id:{} '
396 'src_vol_id:{} snap_id:{}'.format(vol_name, vol_size,
397 img_id, src_vol_id,
398 snap_id))
399 raise RuntimeError(msg)
400
401 # Create new volume
402 try:
403 vol_new = cinder.volumes.create(display_name=vol_name,
404 imageRef=img_id,
405 size=vol_size,
406 source_volid=src_vol_id,
407 snapshot_id=snap_id)
408 vol_id = vol_new.id
409 except Exception as e:
410 msg = 'Failed to create volume: {}'.format(e)
411 raise RuntimeError(msg)
412
413 # Wait for volume to reach available status
414 ret = self.resource_reaches_status(cinder.volumes, vol_id,
415 expected_stat="available",
416 msg="Volume status wait")
417 if not ret:
418 msg = 'Cinder volume failed to reach expected state.'
419 raise RuntimeError(msg)
420
421 # Re-validate new volume
422 self.log.debug('Validating volume attributes...')
423 val_vol_name = cinder.volumes.get(vol_id).display_name
424 val_vol_boot = cinder.volumes.get(vol_id).bootable
425 val_vol_stat = cinder.volumes.get(vol_id).status
426 val_vol_size = cinder.volumes.get(vol_id).size
427 msg_attr = ('Volume attributes - name:{} id:{} stat:{} boot:'
428 '{} size:{}'.format(val_vol_name, vol_id,
429 val_vol_stat, val_vol_boot,
430 val_vol_size))
431
432 if val_vol_boot == bootable and val_vol_stat == 'available' \
433 and val_vol_name == vol_name and val_vol_size == vol_size:
434 self.log.debug(msg_attr)
435 else:
436 msg = ('Volume validation failed, {}'.format(msg_attr))
437 raise RuntimeError(msg)
438
439 return vol_new
440
441 def delete_resource(self, resource, resource_id,
442 msg="resource", max_wait=120):
443 """Delete one openstack resource, such as one instance, keypair,
444 image, volume, stack, etc., and confirm deletion within max wait time.
445
446 :param resource: pointer to os resource type, ex:glance_client.images
447 :param resource_id: unique name or id for the openstack resource
448 :param msg: text to identify purpose in logging
449 :param max_wait: maximum wait time in seconds
450 :returns: True if successful, otherwise False
451 """
452 self.log.debug('Deleting OpenStack resource '
453 '{} ({})'.format(resource_id, msg))
454 num_before = len(list(resource.list()))
455 resource.delete(resource_id)
456
457 tries = 0
458 num_after = len(list(resource.list()))
459 while num_after != (num_before - 1) and tries < (max_wait / 4):
460 self.log.debug('{} delete check: '
461 '{} [{}:{}] {}'.format(msg, tries,
462 num_before,
463 num_after,
464 resource_id))
465 time.sleep(4)
466 num_after = len(list(resource.list()))
467 tries += 1
468
469 self.log.debug('{}: expected, actual count = {}, '
470 '{}'.format(msg, num_before - 1, num_after))
471
472 if num_after == (num_before - 1):
473 return True
474 else:
475 self.log.error('{} delete timed out'.format(msg))
476 return False
477
478 def resource_reaches_status(self, resource, resource_id,
479 expected_stat='available',
480 msg='resource', max_wait=120):
481 """Wait for an openstack resources status to reach an
482 expected status within a specified time. Useful to confirm that
483 nova instances, cinder vols, snapshots, glance images, heat stacks
484 and other resources eventually reach the expected status.
485
486 :param resource: pointer to os resource type, ex: heat_client.stacks
487 :param resource_id: unique id for the openstack resource
488 :param expected_stat: status to expect resource to reach
489 :param msg: text to identify purpose in logging
490 :param max_wait: maximum wait time in seconds
491 :returns: True if successful, False if status is not reached
492 """
493
494 tries = 0
495 resource_stat = resource.get(resource_id).status
496 while resource_stat != expected_stat and tries < (max_wait / 4):
497 self.log.debug('{} status check: '
498 '{} [{}:{}] {}'.format(msg, tries,
499 resource_stat,
500 expected_stat,
501 resource_id))
502 time.sleep(4)
503 resource_stat = resource.get(resource_id).status
504 tries += 1
505
506 self.log.debug('{}: expected, actual status = {}, '
507 '{}'.format(msg, resource_stat, expected_stat))
508
509 if resource_stat == expected_stat:
510 return True
511 else:
512 self.log.debug('{} never reached expected status: '
513 '{}'.format(resource_id, expected_stat))
514 return False
515
516 def get_ceph_osd_id_cmd(self, index):
517 """Produce a shell command that will return a ceph-osd id."""
518 cmd = ("`initctl list | grep 'ceph-osd ' | awk 'NR=={} {{ print $2 }}'"
519 " | grep -o '[0-9]*'`".format(index + 1))
520 return cmd
521
522 def get_ceph_pools(self, sentry_unit):
523 """Return a dict of ceph pools from a single ceph unit, with
524 pool name as keys, pool id as vals."""
525 pools = {}
526 cmd = 'sudo ceph osd lspools'
527 output, code = sentry_unit.run(cmd)
528 if code != 0:
529 msg = ('{} `{}` returned {} '
530 '{}'.format(sentry_unit.info['unit_name'],
531 cmd, code, output))
532 raise RuntimeError(msg)
533
534 # Example output: 0 data,1 metadata,2 rbd,3 cinder,4 glance,
535 for pool in str(output).split(','):
536 pool_id_name = pool.split(' ')
537 if len(pool_id_name) == 2:
538 pool_id = pool_id_name[0]
539 pool_name = pool_id_name[1]
540 pools[pool_name] = int(pool_id)
541
542 self.log.debug('Pools on {}: {}'.format(sentry_unit.info['unit_name'],
543 pools))
544 return pools
545
546 def get_ceph_df(self, sentry_unit):
547 """Return dict of ceph df json output, including ceph pool state.
548
549 :param sentry_unit: Pointer to amulet sentry instance (juju unit)
550 :returns: Dict of ceph df output
551 """
552 cmd = 'sudo ceph df --format=json'
553 output, code = sentry_unit.run(cmd)
554 if code != 0:
555 msg = ('{} `{}` returned {} '
556 '{}'.format(sentry_unit.info['unit_name'],
557 cmd, code, output))
558 raise RuntimeError(msg)
559 return json.loads(output)
560
561 def get_ceph_pool_sample(self, sentry_unit, pool_id=0):
562 """Take a sample of attributes of a ceph pool, returning ceph
563 pool name, object count and disk space used for the specified
564 pool ID number.
565
566 :param sentry_unit: Pointer to amulet sentry instance (juju unit)
567 :param pool_id: Ceph pool ID
568 :returns: List of pool name, object count, kb disk space used
569 """
570 df = self.get_ceph_df(sentry_unit)
571 pool_name = df['pools'][pool_id]['name']
572 obj_count = df['pools'][pool_id]['stats']['objects']
573 kb_used = df['pools'][pool_id]['stats']['kb_used']
574 self.log.debug('Ceph {} pool (ID {}): {} objects, '
575 '{} kb used'.format(pool_name,
576 pool_id,
577 obj_count,
578 kb_used))
579 return pool_name, obj_count, kb_used
580
581 def validate_ceph_pool_samples(self, samples, sample_type="resource pool"):
582 """Validate ceph pool samples taken over time, such as pool
583 object counts or pool kb used, before adding, after adding, and
584 after deleting items which affect those pool attributes. The
585 2nd element is expected to be greater than the 1st; 3rd is expected
586 to be less than the 2nd.
587
588 :param samples: List containing 3 data samples
589 :param sample_type: String for logging and usage context
590 :returns: None if successful, Failure message otherwise
591 """
592 original, created, deleted = range(3)
593 if samples[created] <= samples[original] or \
594 samples[deleted] >= samples[created]:
595 msg = ('Ceph {} samples ({}) '
596 'unexpected.'.format(sample_type, samples))
597 return msg
598 else:
599 self.log.debug('Ceph {} samples (OK): '
600 '{}'.format(sample_type, samples))
601 return None
295602
=== added file 'tests/tests.yaml'
--- tests/tests.yaml 1970-01-01 00:00:00 +0000
+++ tests/tests.yaml 2015-06-30 20:18:04 +0000
@@ -0,0 +1,18 @@
1bootstrap: true
2reset: true
3virtualenv: true
4makefile:
5 - lint
6 - test
7sources:
8 - ppa:juju/stable
9packages:
10 - amulet
11 - python-amulet
12 - python-cinderclient
13 - python-distro-info
14 - python-glanceclient
15 - python-heatclient
16 - python-keystoneclient
17 - python-novaclient
18 - python-swiftclient
019
=== modified file 'unit_tests/test_glance_relations.py'
--- unit_tests/test_glance_relations.py 2015-06-15 17:39:54 +0000
+++ unit_tests/test_glance_relations.py 2015-06-30 20:18:04 +0000
@@ -40,12 +40,13 @@
40 'restart_on_change',40 'restart_on_change',
41 'service_reload',41 'service_reload',
42 'service_stop',42 'service_stop',
43 'service_restart',
43 # charmhelpers.contrib.openstack.utils44 # charmhelpers.contrib.openstack.utils
44 'configure_installation_source',45 'configure_installation_source',
45 'os_release',46 'os_release',
46 'openstack_upgrade_available',47 'openstack_upgrade_available',
47 # charmhelpers.contrib.hahelpers.cluster_utils48 # charmhelpers.contrib.hahelpers.cluster_utils
48 'eligible_leader',49 'is_elected_leader',
49 # glance_utils50 # glance_utils
50 'restart_map',51 'restart_map',
51 'register_configs',52 'register_configs',
@@ -129,10 +130,14 @@
129 self.apt_update.assert_called_with(fatal=True)130 self.apt_update.assert_called_with(fatal=True)
130 self.apt_install.assert_called_with(['haproxy', 'python-setuptools',131 self.apt_install.assert_called_with(['haproxy', 'python-setuptools',
131 'python-six', 'uuid',132 'python-six', 'uuid',
132 'python-mysqldb', 'python-pip',133 'python-mysqldb',
133 'apache2', 'libxslt1-dev',134 'libmysqlclient-dev',
134 'python-psycopg2', 'zlib1g-dev',135 'libssl-dev', 'libffi-dev',
135 'python-dev', 'libxml2-dev'],136 'apache2', 'python-pip',
137 'libxslt1-dev', 'libyaml-dev',
138 'python-psycopg2',
139 'zlib1g-dev', 'python-dev',
140 'libxml2-dev'],
136 fatal=True)141 fatal=True)
137 self.git_install.assert_called_with(projects_yaml)142 self.git_install.assert_called_with(projects_yaml)
138143
@@ -430,6 +435,7 @@
430 self.assertEquals([call('/etc/glance/glance-api.conf'),435 self.assertEquals([call('/etc/glance/glance-api.conf'),
431 call(self.ceph_config_file())],436 call(self.ceph_config_file())],
432 configs.write.call_args_list)437 configs.write.call_args_list)
438 self.service_restart.assert_called_with('glance-api')
433439
434 @patch.object(relations, 'CONFIGS')440 @patch.object(relations, 'CONFIGS')
435 def test_ceph_broken(self, configs):441 def test_ceph_broken(self, configs):
436442
=== modified file 'unit_tests/test_glance_utils.py'
--- unit_tests/test_glance_utils.py 2015-04-17 12:05:48 +0000
+++ unit_tests/test_glance_utils.py 2015-06-30 20:18:04 +0000
@@ -16,13 +16,14 @@
16 'relation_ids',16 'relation_ids',
17 'get_os_codename_install_source',17 'get_os_codename_install_source',
18 'configure_installation_source',18 'configure_installation_source',
19 'eligible_leader',19 'is_elected_leader',
20 'templating',20 'templating',
21 'apt_update',21 'apt_update',
22 'apt_upgrade',22 'apt_upgrade',
23 'apt_install',23 'apt_install',
24 'mkdir',24 'mkdir',
25 'os_release',25 'os_release',
26 'pip_install',
26 'service_start',27 'service_start',
27 'service_stop',28 'service_stop',
28 'service_name',29 'service_name',
@@ -152,7 +153,7 @@
152 git_requested.return_value = True153 git_requested.return_value = True
153 self.config.side_effect = None154 self.config.side_effect = None
154 self.config.return_value = 'cloud:precise-havana'155 self.config.return_value = 'cloud:precise-havana'
155 self.eligible_leader.return_value = True156 self.is_elected_leader.return_value = True
156 self.get_os_codename_install_source.return_value = 'havana'157 self.get_os_codename_install_source.return_value = 'havana'
157 configs = MagicMock()158 configs = MagicMock()
158 utils.do_openstack_upgrade(configs)159 utils.do_openstack_upgrade(configs)
@@ -170,7 +171,7 @@
170 git_requested.return_value = True171 git_requested.return_value = True
171 self.config.side_effect = None172 self.config.side_effect = None
172 self.config.return_value = 'cloud:precise-havana'173 self.config.return_value = 'cloud:precise-havana'
173 self.eligible_leader.return_value = False174 self.is_elected_leader.return_value = False
174 self.get_os_codename_install_source.return_value = 'havana'175 self.get_os_codename_install_source.return_value = 'havana'
175 configs = MagicMock()176 configs = MagicMock()
176 utils.do_openstack_upgrade(configs)177 utils.do_openstack_upgrade(configs)
@@ -236,26 +237,35 @@
236 @patch.object(utils, 'git_src_dir')237 @patch.object(utils, 'git_src_dir')
237 @patch.object(utils, 'service_restart')238 @patch.object(utils, 'service_restart')
238 @patch.object(utils, 'render')239 @patch.object(utils, 'render')
240 @patch.object(utils, 'git_pip_venv_dir')
239 @patch('os.path.join')241 @patch('os.path.join')
240 @patch('os.path.exists')242 @patch('os.path.exists')
243 @patch('os.symlink')
241 @patch('shutil.copytree')244 @patch('shutil.copytree')
242 @patch('shutil.rmtree')245 @patch('shutil.rmtree')
243 def test_git_post_install(self, rmtree, copytree, exists, join, render,246 @patch('subprocess.check_call')
244 service_restart, git_src_dir):247 def test_git_post_install(self, check_call, rmtree, copytree, symlink,
248 exists, join, venv, render, service_restart,
249 git_src_dir):
245 projects_yaml = openstack_origin_git250 projects_yaml = openstack_origin_git
246 join.return_value = 'joined-string'251 join.return_value = 'joined-string'
252 venv.return_value = '/mnt/openstack-git/venv'
247 utils.git_post_install(projects_yaml)253 utils.git_post_install(projects_yaml)
248 expected = [254 expected = [
249 call('joined-string', '/etc/glance'),255 call('joined-string', '/etc/glance'),
250 ]256 ]
251 copytree.assert_has_calls(expected)257 copytree.assert_has_calls(expected)
258 expected = [
259 call('joined-string', '/usr/local/bin/glance-manage'),
260 ]
261 symlink.assert_has_calls(expected, any_order=True)
252 glance_api_context = {262 glance_api_context = {
253 'service_description': 'Glance API server',263 'service_description': 'Glance API server',
254 'service_name': 'Glance',264 'service_name': 'Glance',
255 'user_name': 'glance',265 'user_name': 'glance',
256 'start_dir': '/var/lib/glance',266 'start_dir': '/var/lib/glance',
257 'process_name': 'glance-api',267 'process_name': 'glance-api',
258 'executable_name': '/usr/local/bin/glance-api',268 'executable_name': 'joined-string',
259 'config_files': ['/etc/glance/glance-api.conf'],269 'config_files': ['/etc/glance/glance-api.conf'],
260 'log_file': '/var/log/glance/api.log',270 'log_file': '/var/log/glance/api.log',
261 }271 }
@@ -265,7 +275,7 @@
265 'user_name': 'glance',275 'user_name': 'glance',
266 'start_dir': '/var/lib/glance',276 'start_dir': '/var/lib/glance',
267 'process_name': 'glance-registry',277 'process_name': 'glance-registry',
268 'executable_name': '/usr/local/bin/glance-registry',278 'executable_name': 'joined-string',
269 'config_files': ['/etc/glance/glance-registry.conf'],279 'config_files': ['/etc/glance/glance-registry.conf'],
270 'log_file': '/var/log/glance/registry.log',280 'log_file': '/var/log/glance/registry.log',
271 }281 }

Subscribers

People subscribed via source and target branches