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/>.
22
import glanceclient.v1.client as glance_client
23
import keystoneclient.v2_0 as keystone_client
24
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 charms.
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.
56
self.log.debug('endpoint: {}'.format(repr(ep)))
57
if (admin_port in ep.adminurl and
58
internal_port in ep.internalurl and
59
public_port in ep.publicurl):
61
actual = {'id': ep.id,
63
'adminurl': ep.adminurl,
64
'internalurl': ep.internalurl,
65
'publicurl': ep.publicurl,
66
'service_id': ep.service_id}
67
ret = self._validate_dict_data(expected, actual)
69
return 'unexpected endpoint data - {}'.format(ret)
72
return 'endpoint not found'
74
def validate_svc_catalog_endpoint_data(self, expected, actual):
75
"""Validate service catalog endpoint data.
77
Validate a list of actual service catalog endpoints vs a list of
78
expected service catalog endpoints.
80
self.log.debug('actual: {}'.format(repr(actual)))
81
for k, v in six.iteritems(expected):
83
ret = self._validate_dict_data(expected[k][0], actual[k][0])
85
return self.endpoint_error(k, ret)
87
return "endpoint {} does not exist".format(k)
90
def validate_tenant_data(self, expected, actual):
91
"""Validate tenant data.
93
Validate a list of actual tenant data vs list of expected tenant
96
self.log.debug('actual: {}'.format(repr(actual)))
100
a = {'enabled': act.enabled, 'description': act.description,
101
'name': act.name, 'id': act.id}
102
if e['name'] == a['name']:
104
ret = self._validate_dict_data(e, a)
106
return "unexpected tenant data - {}".format(ret)
108
return "tenant {} does not exist".format(e['name'])
111
def validate_role_data(self, expected, actual):
112
"""Validate role data.
114
Validate a list of actual role data vs a list of expected role
117
self.log.debug('actual: {}'.format(repr(actual)))
121
a = {'name': act.name, 'id': act.id}
122
if e['name'] == a['name']:
124
ret = self._validate_dict_data(e, a)
126
return "unexpected role data - {}".format(ret)
128
return "role {} does not exist".format(e['name'])
131
def validate_user_data(self, expected, actual):
132
"""Validate user data.
134
Validate a list of actual user data vs a list of expected user
137
self.log.debug('actual: {}'.format(repr(actual)))
141
a = {'enabled': act.enabled, 'name': act.name,
142
'email': act.email, 'tenantId': act.tenantId,
144
if e['name'] == a['name']:
146
ret = self._validate_dict_data(e, a)
148
return "unexpected user data - {}".format(ret)
150
return "user {} does not exist".format(e['name'])
153
def validate_flavor_data(self, expected, actual):
154
"""Validate flavor data.
156
Validate a list of actual flavors vs a list of expected flavors.
158
self.log.debug('actual: {}'.format(repr(actual)))
159
act = [a.name for a in actual]
160
return self._validate_list_data(expected, act)
162
def tenant_exists(self, keystone, tenant):
163
"""Return True if tenant exists."""
164
return tenant in [t.name for t in keystone.tenants.list()]
166
def authenticate_keystone_admin(self, keystone_sentry, user, password,
168
"""Authenticates admin user with the keystone admin endpoint."""
169
unit = keystone_sentry
170
service_ip = unit.relation('shared-db',
171
'mysql:shared-db')['private-address']
172
ep = "http://{}:35357/v2.0".format(service_ip.strip().decode('utf-8'))
173
return keystone_client.Client(username=user, password=password,
174
tenant_name=tenant, auth_url=ep)
176
def authenticate_keystone_user(self, keystone, user, password, tenant):
177
"""Authenticates a regular user with the keystone public endpoint."""
178
ep = keystone.service_catalog.url_for(service_type='identity',
179
endpoint_type='publicURL')
180
return keystone_client.Client(username=user, password=password,
181
tenant_name=tenant, auth_url=ep)
183
def authenticate_glance_admin(self, keystone):
184
"""Authenticates admin user with glance."""
185
ep = keystone.service_catalog.url_for(service_type='image',
186
endpoint_type='adminURL')
187
return glance_client.Client(ep, token=keystone.auth_token)
189
def authenticate_nova_user(self, keystone, user, password, tenant):
190
"""Authenticates a regular user with nova-api."""
191
ep = keystone.service_catalog.url_for(service_type='identity',
192
endpoint_type='publicURL')
193
return nova_client.Client(username=user, api_key=password,
194
project_id=tenant, auth_url=ep)
196
def create_cirros_image(self, glance, image_name):
197
"""Download the latest cirros image and upload it to glance."""
198
http_proxy = os.getenv('AMULET_HTTP_PROXY')
199
self.log.debug('AMULET_HTTP_PROXY: {}'.format(http_proxy))
201
proxies = {'http': http_proxy}
202
opener = urllib.FancyURLopener(proxies)
204
opener = urllib.FancyURLopener()
206
f = opener.open("http://download.cirros-cloud.net/version/released")
207
version = f.read().strip()
208
cirros_img = "cirros-{}-x86_64-disk.img".format(version)
209
local_path = os.path.join('tests', cirros_img)
211
if not os.path.exists(local_path):
212
cirros_url = "http://{}/{}/{}".format("download.cirros-cloud.net",
214
opener.retrieve(cirros_url, local_path)
217
with open(local_path) as f:
218
image = glance.images.create(name=image_name, is_public=True,
220
container_format='bare', data=f)
222
status = image.status
223
while status != 'active' and count < 10:
225
image = glance.images.get(image.id)
226
status = image.status
227
self.log.debug('image status: {}'.format(status))
230
if status != 'active':
231
self.log.error('image creation timed out')
236
def delete_image(self, glance, image):
237
"""Delete the specified image."""
238
num_before = len(list(glance.images.list()))
239
glance.images.delete(image)
242
num_after = len(list(glance.images.list()))
243
while num_after != (num_before - 1) and count < 10:
245
num_after = len(list(glance.images.list()))
246
self.log.debug('number of images: {}'.format(num_after))
249
if num_after != (num_before - 1):
250
self.log.error('image deletion timed out')
255
def create_instance(self, nova, image_name, instance_name, flavor):
256
"""Create the specified instance."""
257
image = nova.images.find(name=image_name)
258
flavor = nova.flavors.find(name=flavor)
259
instance = nova.servers.create(name=instance_name, image=image,
263
status = instance.status
264
while status != 'ACTIVE' and count < 60:
266
instance = nova.servers.get(instance.id)
267
status = instance.status
268
self.log.debug('instance status: {}'.format(status))
271
if status != 'ACTIVE':
272
self.log.error('instance creation timed out')
277
def delete_instance(self, nova, instance):
278
"""Delete the specified instance."""
279
num_before = len(list(nova.servers.list()))
280
nova.servers.delete(instance)
283
num_after = len(list(nova.servers.list()))
284
while num_after != (num_before - 1) and count < 10:
286
num_after = len(list(nova.servers.list()))
287
self.log.debug('number of instances: {}'.format(num_after))
290
if num_after != (num_before - 1):
291
self.log.error('instance deletion timed out')