~thomir-deactivatedaccount/charms/trusty/tanuki-result-enum-worker/trunk

« back to all changes in this revision

Viewing changes to hooks/charmhelpers/contrib/openstack/amulet/utils.py

  • Committer: Thomi Richards
  • Date: 2015-06-16 04:35:51 UTC
  • Revision ID: thomi.richards@canonical.com-20150616043551-6efyisjvm20vefq3
Add missing bits of charmhelpers.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright 2014-2015 Canonical Limited.
 
2
#
 
3
# This file is part of charm-helpers.
 
4
#
 
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.
 
8
#
 
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.
 
13
#
 
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/>.
 
16
 
 
17
import logging
 
18
import os
 
19
import six
 
20
import time
 
21
import urllib
 
22
 
 
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
 
27
 
 
28
from charmhelpers.contrib.amulet.utils import (
 
29
    AmuletUtils
 
30
)
 
31
 
 
32
DEBUG = logging.DEBUG
 
33
ERROR = logging.ERROR
 
34
 
 
35
 
 
36
class OpenStackAmuletUtils(AmuletUtils):
 
37
    """OpenStack amulet utilities.
 
38
 
 
39
       This class inherits from AmuletUtils and has additional support
 
40
       that is specifically for use by OpenStack charm tests.
 
41
       """
 
42
 
 
43
    def __init__(self, log_level=ERROR):
 
44
        """Initialize the deployment environment."""
 
45
        super(OpenStackAmuletUtils, self).__init__(log_level)
 
46
 
 
47
    def validate_endpoint_data(self, endpoints, admin_port, internal_port,
 
48
                               public_port, expected):
 
49
        """Validate endpoint data.
 
50
 
 
51
           Validate actual endpoint data vs expected endpoint data. The ports
 
52
           are used to find the matching endpoint.
 
53
           """
 
54
        self.log.debug('Validating endpoint data...')
 
55
        self.log.debug('actual: {}'.format(repr(endpoints)))
 
56
        found = False
 
57
        for ep in 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):
 
62
                found = True
 
63
                actual = {'id': ep.id,
 
64
                          'region': ep.region,
 
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)
 
70
                if ret:
 
71
                    return 'unexpected endpoint data - {}'.format(ret)
 
72
 
 
73
        if not found:
 
74
            return 'endpoint not found'
 
75
 
 
76
    def validate_svc_catalog_endpoint_data(self, expected, actual):
 
77
        """Validate service catalog endpoint data.
 
78
 
 
79
           Validate a list of actual service catalog endpoints vs a list of
 
80
           expected service catalog endpoints.
 
81
           """
 
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):
 
85
            if k in actual:
 
86
                ret = self._validate_dict_data(expected[k][0], actual[k][0])
 
87
                if ret:
 
88
                    return self.endpoint_error(k, ret)
 
89
            else:
 
90
                return "endpoint {} does not exist".format(k)
 
91
        return ret
 
92
 
 
93
    def validate_tenant_data(self, expected, actual):
 
94
        """Validate tenant data.
 
95
 
 
96
           Validate a list of actual tenant data vs list of expected tenant
 
97
           data.
 
98
           """
 
99
        self.log.debug('Validating tenant data...')
 
100
        self.log.debug('actual: {}'.format(repr(actual)))
 
101
        for e in expected:
 
102
            found = False
 
103
            for act in actual:
 
104
                a = {'enabled': act.enabled, 'description': act.description,
 
105
                     'name': act.name, 'id': act.id}
 
106
                if e['name'] == a['name']:
 
107
                    found = True
 
108
                    ret = self._validate_dict_data(e, a)
 
109
                    if ret:
 
110
                        return "unexpected tenant data - {}".format(ret)
 
111
            if not found:
 
112
                return "tenant {} does not exist".format(e['name'])
 
113
        return ret
 
114
 
 
115
    def validate_role_data(self, expected, actual):
 
116
        """Validate role data.
 
117
 
 
118
           Validate a list of actual role data vs a list of expected role
 
119
           data.
 
120
           """
 
121
        self.log.debug('Validating role data...')
 
122
        self.log.debug('actual: {}'.format(repr(actual)))
 
123
        for e in expected:
 
124
            found = False
 
125
            for act in actual:
 
126
                a = {'name': act.name, 'id': act.id}
 
127
                if e['name'] == a['name']:
 
128
                    found = True
 
129
                    ret = self._validate_dict_data(e, a)
 
130
                    if ret:
 
131
                        return "unexpected role data - {}".format(ret)
 
132
            if not found:
 
133
                return "role {} does not exist".format(e['name'])
 
134
        return ret
 
135
 
 
136
    def validate_user_data(self, expected, actual):
 
137
        """Validate user data.
 
138
 
 
139
           Validate a list of actual user data vs a list of expected user
 
140
           data.
 
141
           """
 
142
        self.log.debug('Validating user data...')
 
143
        self.log.debug('actual: {}'.format(repr(actual)))
 
144
        for e in expected:
 
145
            found = False
 
146
            for act in actual:
 
147
                a = {'enabled': act.enabled, 'name': act.name,
 
148
                     'email': act.email, 'tenantId': act.tenantId,
 
149
                     'id': act.id}
 
150
                if e['name'] == a['name']:
 
151
                    found = True
 
152
                    ret = self._validate_dict_data(e, a)
 
153
                    if ret:
 
154
                        return "unexpected user data - {}".format(ret)
 
155
            if not found:
 
156
                return "user {} does not exist".format(e['name'])
 
157
        return ret
 
158
 
 
159
    def validate_flavor_data(self, expected, actual):
 
160
        """Validate flavor data.
 
161
 
 
162
           Validate a list of actual flavors vs a list of expected flavors.
 
163
           """
 
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)
 
168
 
 
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()]
 
173
 
 
174
    def authenticate_keystone_admin(self, keystone_sentry, user, password,
 
175
                                    tenant):
 
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)
 
184
 
 
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)
 
192
 
 
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)
 
199
 
 
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
 
 
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)
 
214
 
 
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))
 
220
        if http_proxy:
 
221
            proxies = {'http': http_proxy}
 
222
            opener = urllib.FancyURLopener(proxies)
 
223
        else:
 
224
            opener = urllib.FancyURLopener()
 
225
 
 
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)
 
230
 
 
231
        if not os.path.exists(local_path):
 
232
            cirros_url = "http://{}/{}/{}".format("download.cirros-cloud.net",
 
233
                                                  version, cirros_img)
 
234
            opener.retrieve(cirros_url, local_path)
 
235
        f.close()
 
236
 
 
237
        with open(local_path) as f:
 
238
            image = glance.images.create(name=image_name, is_public=True,
 
239
                                         disk_format='qcow2',
 
240
                                         container_format='bare', data=f)
 
241
        count = 1
 
242
        status = image.status
 
243
        while status != 'active' and count < 10:
 
244
            time.sleep(3)
 
245
            image = glance.images.get(image.id)
 
246
            status = image.status
 
247
            self.log.debug('image status: {}'.format(status))
 
248
            count += 1
 
249
 
 
250
        if status != 'active':
 
251
            self.log.error('image creation timed out')
 
252
            return None
 
253
 
 
254
        return image
 
255
 
 
256
    def delete_image(self, glance, 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))
 
263
        num_before = len(list(glance.images.list()))
 
264
        glance.images.delete(image)
 
265
 
 
266
        count = 1
 
267
        num_after = len(list(glance.images.list()))
 
268
        while num_after != (num_before - 1) and count < 10:
 
269
            time.sleep(3)
 
270
            num_after = len(list(glance.images.list()))
 
271
            self.log.debug('number of images: {}'.format(num_after))
 
272
            count += 1
 
273
 
 
274
        if num_after != (num_before - 1):
 
275
            self.log.error('image deletion timed out')
 
276
            return False
 
277
 
 
278
        return True
 
279
 
 
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,
 
287
                                       flavor=flavor)
 
288
 
 
289
        count = 1
 
290
        status = instance.status
 
291
        while status != 'ACTIVE' and count < 60:
 
292
            time.sleep(3)
 
293
            instance = nova.servers.get(instance.id)
 
294
            status = instance.status
 
295
            self.log.debug('instance status: {}'.format(status))
 
296
            count += 1
 
297
 
 
298
        if status != 'ACTIVE':
 
299
            self.log.error('instance creation timed out')
 
300
            return None
 
301
 
 
302
        return instance
 
303
 
 
304
    def delete_instance(self, nova, 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))
 
311
        num_before = len(list(nova.servers.list()))
 
312
        nova.servers.delete(instance)
 
313
 
 
314
        count = 1
 
315
        num_after = len(list(nova.servers.list()))
 
316
        while num_after != (num_before - 1) and count < 10:
 
317
            time.sleep(3)
 
318
            num_after = len(list(nova.servers.list()))
 
319
            self.log.debug('number of instances: {}'.format(num_after))
 
320
            count += 1
 
321
 
 
322
        if num_after != (num_before - 1):
 
323
            self.log.error('instance deletion timed out')
 
324
            return False
 
325
 
 
326
        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