1
# Copyright 2014-2015 Canonical Limited.
3
# This file is part of charm-helpers.
5
# charm-helpers is free software: you can redistribute it and/or modify
6
# it under the terms of the GNU Lesser General Public License version 3 as
7
# published by the Free Software Foundation.
9
# charm-helpers is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
# GNU Lesser General Public License for more details.
14
# 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/>.
23
import glanceclient.v1.client as glance_client
24
import heatclient.v1.client as heat_client
25
import keystoneclient.v2_0 as keystone_client
26
import novaclient.v1_1.client as nova_client
28
from charmhelpers.contrib.amulet.utils import (
36
class OpenStackAmuletUtils(AmuletUtils):
37
"""OpenStack amulet utilities.
39
This class inherits from AmuletUtils and has additional support
40
that is specifically for use by OpenStack charm tests.
43
def __init__(self, log_level=ERROR):
44
"""Initialize the deployment environment."""
45
super(OpenStackAmuletUtils, self).__init__(log_level)
47
def validate_endpoint_data(self, endpoints, admin_port, internal_port,
48
public_port, expected):
49
"""Validate endpoint data.
51
Validate actual endpoint data vs expected endpoint data. The ports
52
are used to find the matching endpoint.
54
self.log.debug('Validating endpoint data...')
55
self.log.debug('actual: {}'.format(repr(endpoints)))
58
self.log.debug('endpoint: {}'.format(repr(ep)))
59
if (admin_port in ep.adminurl and
60
internal_port in ep.internalurl and
61
public_port in ep.publicurl):
63
actual = {'id': ep.id,
65
'adminurl': ep.adminurl,
66
'internalurl': ep.internalurl,
67
'publicurl': ep.publicurl,
68
'service_id': ep.service_id}
69
ret = self._validate_dict_data(expected, actual)
71
return 'unexpected endpoint data - {}'.format(ret)
74
return 'endpoint not found'
76
def validate_svc_catalog_endpoint_data(self, expected, actual):
77
"""Validate service catalog endpoint data.
79
Validate a list of actual service catalog endpoints vs a list of
80
expected service catalog endpoints.
82
self.log.debug('Validating service catalog endpoint data...')
83
self.log.debug('actual: {}'.format(repr(actual)))
84
for k, v in six.iteritems(expected):
86
ret = self._validate_dict_data(expected[k][0], actual[k][0])
88
return self.endpoint_error(k, ret)
90
return "endpoint {} does not exist".format(k)
93
def validate_tenant_data(self, expected, actual):
94
"""Validate tenant data.
96
Validate a list of actual tenant data vs list of expected tenant
99
self.log.debug('Validating tenant data...')
100
self.log.debug('actual: {}'.format(repr(actual)))
104
a = {'enabled': act.enabled, 'description': act.description,
105
'name': act.name, 'id': act.id}
106
if e['name'] == a['name']:
108
ret = self._validate_dict_data(e, a)
110
return "unexpected tenant data - {}".format(ret)
112
return "tenant {} does not exist".format(e['name'])
115
def validate_role_data(self, expected, actual):
116
"""Validate role data.
118
Validate a list of actual role data vs a list of expected role
121
self.log.debug('Validating role data...')
122
self.log.debug('actual: {}'.format(repr(actual)))
126
a = {'name': act.name, 'id': act.id}
127
if e['name'] == a['name']:
129
ret = self._validate_dict_data(e, a)
131
return "unexpected role data - {}".format(ret)
133
return "role {} does not exist".format(e['name'])
136
def validate_user_data(self, expected, actual):
137
"""Validate user data.
139
Validate a list of actual user data vs a list of expected user
142
self.log.debug('Validating user data...')
143
self.log.debug('actual: {}'.format(repr(actual)))
147
a = {'enabled': act.enabled, 'name': act.name,
148
'email': act.email, 'tenantId': act.tenantId,
150
if e['name'] == a['name']:
152
ret = self._validate_dict_data(e, a)
154
return "unexpected user data - {}".format(ret)
156
return "user {} does not exist".format(e['name'])
159
def validate_flavor_data(self, expected, actual):
160
"""Validate flavor data.
162
Validate a list of actual flavors vs a list of expected flavors.
164
self.log.debug('Validating flavor data...')
165
self.log.debug('actual: {}'.format(repr(actual)))
166
act = [a.name for a in actual]
167
return self._validate_list_data(expected, act)
169
def tenant_exists(self, keystone, tenant):
170
"""Return True if tenant exists."""
171
self.log.debug('Checking if tenant exists ({})...'.format(tenant))
172
return tenant in [t.name for t in keystone.tenants.list()]
174
def authenticate_keystone_admin(self, keystone_sentry, user, password,
176
"""Authenticates admin user with the keystone admin endpoint."""
177
self.log.debug('Authenticating keystone admin...')
178
unit = keystone_sentry
179
service_ip = unit.relation('shared-db',
180
'mysql:shared-db')['private-address']
181
ep = "http://{}:35357/v2.0".format(service_ip.strip().decode('utf-8'))
182
return keystone_client.Client(username=user, password=password,
183
tenant_name=tenant, auth_url=ep)
185
def authenticate_keystone_user(self, keystone, user, password, tenant):
186
"""Authenticates a regular user with the keystone public endpoint."""
187
self.log.debug('Authenticating keystone user ({})...'.format(user))
188
ep = keystone.service_catalog.url_for(service_type='identity',
189
endpoint_type='publicURL')
190
return keystone_client.Client(username=user, password=password,
191
tenant_name=tenant, auth_url=ep)
193
def authenticate_glance_admin(self, keystone):
194
"""Authenticates admin user with glance."""
195
self.log.debug('Authenticating glance admin...')
196
ep = keystone.service_catalog.url_for(service_type='image',
197
endpoint_type='adminURL')
198
return glance_client.Client(ep, token=keystone.auth_token)
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)
207
def authenticate_nova_user(self, keystone, user, password, tenant):
208
"""Authenticates a regular user with nova-api."""
209
self.log.debug('Authenticating nova user ({})...'.format(user))
210
ep = keystone.service_catalog.url_for(service_type='identity',
211
endpoint_type='publicURL')
212
return nova_client.Client(username=user, api_key=password,
213
project_id=tenant, auth_url=ep)
215
def create_cirros_image(self, glance, image_name):
216
"""Download the latest cirros image and upload it to glance."""
217
self.log.debug('Creating glance image ({})...'.format(image_name))
218
http_proxy = os.getenv('AMULET_HTTP_PROXY')
219
self.log.debug('AMULET_HTTP_PROXY: {}'.format(http_proxy))
221
proxies = {'http': http_proxy}
222
opener = urllib.FancyURLopener(proxies)
224
opener = urllib.FancyURLopener()
226
f = opener.open("http://download.cirros-cloud.net/version/released")
227
version = f.read().strip()
228
cirros_img = "cirros-{}-x86_64-disk.img".format(version)
229
local_path = os.path.join('tests', cirros_img)
231
if not os.path.exists(local_path):
232
cirros_url = "http://{}/{}/{}".format("download.cirros-cloud.net",
234
opener.retrieve(cirros_url, local_path)
237
with open(local_path) as f:
238
image = glance.images.create(name=image_name, is_public=True,
240
container_format='bare', data=f)
242
status = image.status
243
while status != 'active' and count < 10:
245
image = glance.images.get(image.id)
246
status = image.status
247
self.log.debug('image status: {}'.format(status))
250
if status != 'active':
251
self.log.error('image creation timed out')
256
def delete_image(self, glance, image):
257
"""Delete the specified image."""
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))
263
num_before = len(list(glance.images.list()))
264
glance.images.delete(image)
267
num_after = len(list(glance.images.list()))
268
while num_after != (num_before - 1) and count < 10:
270
num_after = len(list(glance.images.list()))
271
self.log.debug('number of images: {}'.format(num_after))
274
if num_after != (num_before - 1):
275
self.log.error('image deletion timed out')
280
def create_instance(self, nova, image_name, instance_name, flavor):
281
"""Create the specified instance."""
282
self.log.debug('Creating instance '
283
'({}|{}|{})'.format(instance_name, image_name, flavor))
284
image = nova.images.find(name=image_name)
285
flavor = nova.flavors.find(name=flavor)
286
instance = nova.servers.create(name=instance_name, image=image,
290
status = instance.status
291
while status != 'ACTIVE' and count < 60:
293
instance = nova.servers.get(instance.id)
294
status = instance.status
295
self.log.debug('instance status: {}'.format(status))
298
if status != 'ACTIVE':
299
self.log.error('instance creation timed out')
304
def delete_instance(self, nova, instance):
305
"""Delete the specified instance."""
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))
311
num_before = len(list(nova.servers.list()))
312
nova.servers.delete(instance)
315
num_after = len(list(nova.servers.list()))
316
while num_after != (num_before - 1) and count < 10:
318
num_after = len(list(nova.servers.list()))
319
self.log.debug('number of instances: {}'.format(num_after))
322
if num_after != (num_before - 1):
323
self.log.error('instance deletion timed out')
328
def create_or_get_keypair(self, nova, keypair_name="testkey"):
329
"""Create a new keypair, or return pointer if it already exists."""
331
_keypair = nova.keypairs.get(keypair_name)
332
self.log.debug('Keypair ({}) already exists, '
333
'using it.'.format(keypair_name))
336
self.log.debug('Keypair ({}) does not exist, '
337
'creating it.'.format(keypair_name))
339
_keypair = nova.keypairs.create(name=keypair_name)
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.
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
353
num_before = len(list(resource.list()))
354
resource.delete(resource_id)
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,
365
num_after = len(list(resource.list()))
368
self.log.debug('{}: expected, actual count = {}, '
369
'{}'.format(msg, num_before - 1, num_after))
371
if num_after == (num_before - 1):
374
self.log.error('{} delete timed out'.format(msg))
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.
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
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,
402
resource_stat = resource.get(resource_id).status
405
self.log.debug('{}: expected, actual status = {}, '
406
'{}'.format(msg, resource_stat, expected_stat))
408
if resource_stat == expected_stat:
411
self.log.debug('{} never reached expected status: '
412
'{}'.format(resource_id, expected_stat))