~hopem/charms/trusty/ceilometer-agent/lp1535062

« back to all changes in this revision

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

  • Committer: james.page at ubuntu
  • Date: 2015-07-07 13:56:22 UTC
  • mfrom: (55.1.11 ceilometer-agent)
  • Revision ID: james.page@ubuntu.com-20150707135622-th05erkkiftvs8k2
Carry over amulet tests from ceilometer as a baseline; Add subordinate relation, service catalog, endpoint and nova ceilometer config checks.

Fix lint in unit test (unused import).

Resolve grizzly-override assumptions re: bug 1469241.

Remove accidental templates/ceilometer.conf.~1~ from rev23 circa 2013.

Update tags for consistency with other os-charms.

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 amulet
 
18
import json
 
19
import logging
 
20
import os
 
21
import six
 
22
import time
 
23
import urllib
 
24
 
 
25
import cinderclient.v1.client as cinder_client
 
26
import glanceclient.v1.client as glance_client
 
27
import heatclient.v1.client as heat_client
 
28
import keystoneclient.v2_0 as keystone_client
 
29
import novaclient.v1_1.client as nova_client
 
30
import swiftclient
 
31
 
 
32
from charmhelpers.contrib.amulet.utils import (
 
33
    AmuletUtils
 
34
)
 
35
 
 
36
DEBUG = logging.DEBUG
 
37
ERROR = logging.ERROR
 
38
 
 
39
 
 
40
class OpenStackAmuletUtils(AmuletUtils):
 
41
    """OpenStack amulet utilities.
 
42
 
 
43
       This class inherits from AmuletUtils and has additional support
 
44
       that is specifically for use by OpenStack charm tests.
 
45
       """
 
46
 
 
47
    def __init__(self, log_level=ERROR):
 
48
        """Initialize the deployment environment."""
 
49
        super(OpenStackAmuletUtils, self).__init__(log_level)
 
50
 
 
51
    def validate_endpoint_data(self, endpoints, admin_port, internal_port,
 
52
                               public_port, expected):
 
53
        """Validate endpoint data.
 
54
 
 
55
           Validate actual endpoint data vs expected endpoint data. The ports
 
56
           are used to find the matching endpoint.
 
57
           """
 
58
        self.log.debug('Validating endpoint data...')
 
59
        self.log.debug('actual: {}'.format(repr(endpoints)))
 
60
        found = False
 
61
        for ep in endpoints:
 
62
            self.log.debug('endpoint: {}'.format(repr(ep)))
 
63
            if (admin_port in ep.adminurl and
 
64
                    internal_port in ep.internalurl and
 
65
                    public_port in ep.publicurl):
 
66
                found = True
 
67
                actual = {'id': ep.id,
 
68
                          'region': ep.region,
 
69
                          'adminurl': ep.adminurl,
 
70
                          'internalurl': ep.internalurl,
 
71
                          'publicurl': ep.publicurl,
 
72
                          'service_id': ep.service_id}
 
73
                ret = self._validate_dict_data(expected, actual)
 
74
                if ret:
 
75
                    return 'unexpected endpoint data - {}'.format(ret)
 
76
 
 
77
        if not found:
 
78
            return 'endpoint not found'
 
79
 
 
80
    def validate_svc_catalog_endpoint_data(self, expected, actual):
 
81
        """Validate service catalog endpoint data.
 
82
 
 
83
           Validate a list of actual service catalog endpoints vs a list of
 
84
           expected service catalog endpoints.
 
85
           """
 
86
        self.log.debug('Validating service catalog endpoint data...')
 
87
        self.log.debug('actual: {}'.format(repr(actual)))
 
88
        for k, v in six.iteritems(expected):
 
89
            if k in actual:
 
90
                ret = self._validate_dict_data(expected[k][0], actual[k][0])
 
91
                if ret:
 
92
                    return self.endpoint_error(k, ret)
 
93
            else:
 
94
                return "endpoint {} does not exist".format(k)
 
95
        return ret
 
96
 
 
97
    def validate_tenant_data(self, expected, actual):
 
98
        """Validate tenant data.
 
99
 
 
100
           Validate a list of actual tenant data vs list of expected tenant
 
101
           data.
 
102
           """
 
103
        self.log.debug('Validating tenant data...')
 
104
        self.log.debug('actual: {}'.format(repr(actual)))
 
105
        for e in expected:
 
106
            found = False
 
107
            for act in actual:
 
108
                a = {'enabled': act.enabled, 'description': act.description,
 
109
                     'name': act.name, 'id': act.id}
 
110
                if e['name'] == a['name']:
 
111
                    found = True
 
112
                    ret = self._validate_dict_data(e, a)
 
113
                    if ret:
 
114
                        return "unexpected tenant data - {}".format(ret)
 
115
            if not found:
 
116
                return "tenant {} does not exist".format(e['name'])
 
117
        return ret
 
118
 
 
119
    def validate_role_data(self, expected, actual):
 
120
        """Validate role data.
 
121
 
 
122
           Validate a list of actual role data vs a list of expected role
 
123
           data.
 
124
           """
 
125
        self.log.debug('Validating role data...')
 
126
        self.log.debug('actual: {}'.format(repr(actual)))
 
127
        for e in expected:
 
128
            found = False
 
129
            for act in actual:
 
130
                a = {'name': act.name, 'id': act.id}
 
131
                if e['name'] == a['name']:
 
132
                    found = True
 
133
                    ret = self._validate_dict_data(e, a)
 
134
                    if ret:
 
135
                        return "unexpected role data - {}".format(ret)
 
136
            if not found:
 
137
                return "role {} does not exist".format(e['name'])
 
138
        return ret
 
139
 
 
140
    def validate_user_data(self, expected, actual):
 
141
        """Validate user data.
 
142
 
 
143
           Validate a list of actual user data vs a list of expected user
 
144
           data.
 
145
           """
 
146
        self.log.debug('Validating user data...')
 
147
        self.log.debug('actual: {}'.format(repr(actual)))
 
148
        for e in expected:
 
149
            found = False
 
150
            for act in actual:
 
151
                a = {'enabled': act.enabled, 'name': act.name,
 
152
                     'email': act.email, 'tenantId': act.tenantId,
 
153
                     'id': act.id}
 
154
                if e['name'] == a['name']:
 
155
                    found = True
 
156
                    ret = self._validate_dict_data(e, a)
 
157
                    if ret:
 
158
                        return "unexpected user data - {}".format(ret)
 
159
            if not found:
 
160
                return "user {} does not exist".format(e['name'])
 
161
        return ret
 
162
 
 
163
    def validate_flavor_data(self, expected, actual):
 
164
        """Validate flavor data.
 
165
 
 
166
           Validate a list of actual flavors vs a list of expected flavors.
 
167
           """
 
168
        self.log.debug('Validating flavor data...')
 
169
        self.log.debug('actual: {}'.format(repr(actual)))
 
170
        act = [a.name for a in actual]
 
171
        return self._validate_list_data(expected, act)
 
172
 
 
173
    def tenant_exists(self, keystone, tenant):
 
174
        """Return True if tenant exists."""
 
175
        self.log.debug('Checking if tenant exists ({})...'.format(tenant))
 
176
        return tenant in [t.name for t in keystone.tenants.list()]
 
177
 
 
178
    def authenticate_cinder_admin(self, keystone_sentry, username,
 
179
                                  password, tenant):
 
180
        """Authenticates admin user with cinder."""
 
181
        # NOTE(beisner): cinder python client doesn't accept tokens.
 
182
        service_ip = \
 
183
            keystone_sentry.relation('shared-db',
 
184
                                     'mysql:shared-db')['private-address']
 
185
        ept = "http://{}:5000/v2.0".format(service_ip.strip().decode('utf-8'))
 
186
        return cinder_client.Client(username, password, tenant, ept)
 
187
 
 
188
    def authenticate_keystone_admin(self, keystone_sentry, user, password,
 
189
                                    tenant):
 
190
        """Authenticates admin user with the keystone admin endpoint."""
 
191
        self.log.debug('Authenticating keystone admin...')
 
192
        unit = keystone_sentry
 
193
        service_ip = unit.relation('shared-db',
 
194
                                   'mysql:shared-db')['private-address']
 
195
        ep = "http://{}:35357/v2.0".format(service_ip.strip().decode('utf-8'))
 
196
        return keystone_client.Client(username=user, password=password,
 
197
                                      tenant_name=tenant, auth_url=ep)
 
198
 
 
199
    def authenticate_keystone_user(self, keystone, user, password, tenant):
 
200
        """Authenticates a regular user with the keystone public endpoint."""
 
201
        self.log.debug('Authenticating keystone user ({})...'.format(user))
 
202
        ep = keystone.service_catalog.url_for(service_type='identity',
 
203
                                              endpoint_type='publicURL')
 
204
        return keystone_client.Client(username=user, password=password,
 
205
                                      tenant_name=tenant, auth_url=ep)
 
206
 
 
207
    def authenticate_glance_admin(self, keystone):
 
208
        """Authenticates admin user with glance."""
 
209
        self.log.debug('Authenticating glance admin...')
 
210
        ep = keystone.service_catalog.url_for(service_type='image',
 
211
                                              endpoint_type='adminURL')
 
212
        return glance_client.Client(ep, token=keystone.auth_token)
 
213
 
 
214
    def authenticate_heat_admin(self, keystone):
 
215
        """Authenticates the admin user with heat."""
 
216
        self.log.debug('Authenticating heat admin...')
 
217
        ep = keystone.service_catalog.url_for(service_type='orchestration',
 
218
                                              endpoint_type='publicURL')
 
219
        return heat_client.Client(endpoint=ep, token=keystone.auth_token)
 
220
 
 
221
    def authenticate_nova_user(self, keystone, user, password, tenant):
 
222
        """Authenticates a regular user with nova-api."""
 
223
        self.log.debug('Authenticating nova user ({})...'.format(user))
 
224
        ep = keystone.service_catalog.url_for(service_type='identity',
 
225
                                              endpoint_type='publicURL')
 
226
        return nova_client.Client(username=user, api_key=password,
 
227
                                  project_id=tenant, auth_url=ep)
 
228
 
 
229
    def authenticate_swift_user(self, keystone, user, password, tenant):
 
230
        """Authenticates a regular user with swift api."""
 
231
        self.log.debug('Authenticating swift user ({})...'.format(user))
 
232
        ep = keystone.service_catalog.url_for(service_type='identity',
 
233
                                              endpoint_type='publicURL')
 
234
        return swiftclient.Connection(authurl=ep,
 
235
                                      user=user,
 
236
                                      key=password,
 
237
                                      tenant_name=tenant,
 
238
                                      auth_version='2.0')
 
239
 
 
240
    def create_cirros_image(self, glance, image_name):
 
241
        """Download the latest cirros image and upload it to glance,
 
242
        validate and return a resource pointer.
 
243
 
 
244
        :param glance: pointer to authenticated glance connection
 
245
        :param image_name: display name for new image
 
246
        :returns: glance image pointer
 
247
        """
 
248
        self.log.debug('Creating glance cirros image '
 
249
                       '({})...'.format(image_name))
 
250
 
 
251
        # Download cirros image
 
252
        http_proxy = os.getenv('AMULET_HTTP_PROXY')
 
253
        self.log.debug('AMULET_HTTP_PROXY: {}'.format(http_proxy))
 
254
        if http_proxy:
 
255
            proxies = {'http': http_proxy}
 
256
            opener = urllib.FancyURLopener(proxies)
 
257
        else:
 
258
            opener = urllib.FancyURLopener()
 
259
 
 
260
        f = opener.open('http://download.cirros-cloud.net/version/released')
 
261
        version = f.read().strip()
 
262
        cirros_img = 'cirros-{}-x86_64-disk.img'.format(version)
 
263
        local_path = os.path.join('tests', cirros_img)
 
264
 
 
265
        if not os.path.exists(local_path):
 
266
            cirros_url = 'http://{}/{}/{}'.format('download.cirros-cloud.net',
 
267
                                                  version, cirros_img)
 
268
            opener.retrieve(cirros_url, local_path)
 
269
        f.close()
 
270
 
 
271
        # Create glance image
 
272
        with open(local_path) as f:
 
273
            image = glance.images.create(name=image_name, is_public=True,
 
274
                                         disk_format='qcow2',
 
275
                                         container_format='bare', data=f)
 
276
 
 
277
        # Wait for image to reach active status
 
278
        img_id = image.id
 
279
        ret = self.resource_reaches_status(glance.images, img_id,
 
280
                                           expected_stat='active',
 
281
                                           msg='Image status wait')
 
282
        if not ret:
 
283
            msg = 'Glance image failed to reach expected state.'
 
284
            amulet.raise_status(amulet.FAIL, msg=msg)
 
285
 
 
286
        # Re-validate new image
 
287
        self.log.debug('Validating image attributes...')
 
288
        val_img_name = glance.images.get(img_id).name
 
289
        val_img_stat = glance.images.get(img_id).status
 
290
        val_img_pub = glance.images.get(img_id).is_public
 
291
        val_img_cfmt = glance.images.get(img_id).container_format
 
292
        val_img_dfmt = glance.images.get(img_id).disk_format
 
293
        msg_attr = ('Image attributes - name:{} public:{} id:{} stat:{} '
 
294
                    'container fmt:{} disk fmt:{}'.format(
 
295
                        val_img_name, val_img_pub, img_id,
 
296
                        val_img_stat, val_img_cfmt, val_img_dfmt))
 
297
 
 
298
        if val_img_name == image_name and val_img_stat == 'active' \
 
299
                and val_img_pub is True and val_img_cfmt == 'bare' \
 
300
                and val_img_dfmt == 'qcow2':
 
301
            self.log.debug(msg_attr)
 
302
        else:
 
303
            msg = ('Volume validation failed, {}'.format(msg_attr))
 
304
            amulet.raise_status(amulet.FAIL, msg=msg)
 
305
 
 
306
        return image
 
307
 
 
308
    def delete_image(self, glance, image):
 
309
        """Delete the specified image."""
 
310
 
 
311
        # /!\ DEPRECATION WARNING
 
312
        self.log.warn('/!\\ DEPRECATION WARNING:  use '
 
313
                      'delete_resource instead of delete_image.')
 
314
        self.log.debug('Deleting glance image ({})...'.format(image))
 
315
        return self.delete_resource(glance.images, image, msg='glance image')
 
316
 
 
317
    def create_instance(self, nova, image_name, instance_name, flavor):
 
318
        """Create the specified instance."""
 
319
        self.log.debug('Creating instance '
 
320
                       '({}|{}|{})'.format(instance_name, image_name, flavor))
 
321
        image = nova.images.find(name=image_name)
 
322
        flavor = nova.flavors.find(name=flavor)
 
323
        instance = nova.servers.create(name=instance_name, image=image,
 
324
                                       flavor=flavor)
 
325
 
 
326
        count = 1
 
327
        status = instance.status
 
328
        while status != 'ACTIVE' and count < 60:
 
329
            time.sleep(3)
 
330
            instance = nova.servers.get(instance.id)
 
331
            status = instance.status
 
332
            self.log.debug('instance status: {}'.format(status))
 
333
            count += 1
 
334
 
 
335
        if status != 'ACTIVE':
 
336
            self.log.error('instance creation timed out')
 
337
            return None
 
338
 
 
339
        return instance
 
340
 
 
341
    def delete_instance(self, nova, instance):
 
342
        """Delete the specified instance."""
 
343
 
 
344
        # /!\ DEPRECATION WARNING
 
345
        self.log.warn('/!\\ DEPRECATION WARNING:  use '
 
346
                      'delete_resource instead of delete_instance.')
 
347
        self.log.debug('Deleting instance ({})...'.format(instance))
 
348
        return self.delete_resource(nova.servers, instance,
 
349
                                    msg='nova instance')
 
350
 
 
351
    def create_or_get_keypair(self, nova, keypair_name="testkey"):
 
352
        """Create a new keypair, or return pointer if it already exists."""
 
353
        try:
 
354
            _keypair = nova.keypairs.get(keypair_name)
 
355
            self.log.debug('Keypair ({}) already exists, '
 
356
                           'using it.'.format(keypair_name))
 
357
            return _keypair
 
358
        except:
 
359
            self.log.debug('Keypair ({}) does not exist, '
 
360
                           'creating it.'.format(keypair_name))
 
361
 
 
362
        _keypair = nova.keypairs.create(name=keypair_name)
 
363
        return _keypair
 
364
 
 
365
    def create_cinder_volume(self, cinder, vol_name="demo-vol", vol_size=1,
 
366
                             img_id=None, src_vol_id=None, snap_id=None):
 
367
        """Create cinder volume, optionally from a glance image, OR
 
368
        optionally as a clone of an existing volume, OR optionally
 
369
        from a snapshot.  Wait for the new volume status to reach
 
370
        the expected status, validate and return a resource pointer.
 
371
 
 
372
        :param vol_name: cinder volume display name
 
373
        :param vol_size: size in gigabytes
 
374
        :param img_id: optional glance image id
 
375
        :param src_vol_id: optional source volume id to clone
 
376
        :param snap_id: optional snapshot id to use
 
377
        :returns: cinder volume pointer
 
378
        """
 
379
        # Handle parameter input and avoid impossible combinations
 
380
        if img_id and not src_vol_id and not snap_id:
 
381
            # Create volume from image
 
382
            self.log.debug('Creating cinder volume from glance image...')
 
383
            bootable = 'true'
 
384
        elif src_vol_id and not img_id and not snap_id:
 
385
            # Clone an existing volume
 
386
            self.log.debug('Cloning cinder volume...')
 
387
            bootable = cinder.volumes.get(src_vol_id).bootable
 
388
        elif snap_id and not src_vol_id and not img_id:
 
389
            # Create volume from snapshot
 
390
            self.log.debug('Creating cinder volume from snapshot...')
 
391
            snap = cinder.volume_snapshots.find(id=snap_id)
 
392
            vol_size = snap.size
 
393
            snap_vol_id = cinder.volume_snapshots.get(snap_id).volume_id
 
394
            bootable = cinder.volumes.get(snap_vol_id).bootable
 
395
        elif not img_id and not src_vol_id and not snap_id:
 
396
            # Create volume
 
397
            self.log.debug('Creating cinder volume...')
 
398
            bootable = 'false'
 
399
        else:
 
400
            # Impossible combination of parameters
 
401
            msg = ('Invalid method use - name:{} size:{} img_id:{} '
 
402
                   'src_vol_id:{} snap_id:{}'.format(vol_name, vol_size,
 
403
                                                     img_id, src_vol_id,
 
404
                                                     snap_id))
 
405
            amulet.raise_status(amulet.FAIL, msg=msg)
 
406
 
 
407
        # Create new volume
 
408
        try:
 
409
            vol_new = cinder.volumes.create(display_name=vol_name,
 
410
                                            imageRef=img_id,
 
411
                                            size=vol_size,
 
412
                                            source_volid=src_vol_id,
 
413
                                            snapshot_id=snap_id)
 
414
            vol_id = vol_new.id
 
415
        except Exception as e:
 
416
            msg = 'Failed to create volume: {}'.format(e)
 
417
            amulet.raise_status(amulet.FAIL, msg=msg)
 
418
 
 
419
        # Wait for volume to reach available status
 
420
        ret = self.resource_reaches_status(cinder.volumes, vol_id,
 
421
                                           expected_stat="available",
 
422
                                           msg="Volume status wait")
 
423
        if not ret:
 
424
            msg = 'Cinder volume failed to reach expected state.'
 
425
            amulet.raise_status(amulet.FAIL, msg=msg)
 
426
 
 
427
        # Re-validate new volume
 
428
        self.log.debug('Validating volume attributes...')
 
429
        val_vol_name = cinder.volumes.get(vol_id).display_name
 
430
        val_vol_boot = cinder.volumes.get(vol_id).bootable
 
431
        val_vol_stat = cinder.volumes.get(vol_id).status
 
432
        val_vol_size = cinder.volumes.get(vol_id).size
 
433
        msg_attr = ('Volume attributes - name:{} id:{} stat:{} boot:'
 
434
                    '{} size:{}'.format(val_vol_name, vol_id,
 
435
                                        val_vol_stat, val_vol_boot,
 
436
                                        val_vol_size))
 
437
 
 
438
        if val_vol_boot == bootable and val_vol_stat == 'available' \
 
439
                and val_vol_name == vol_name and val_vol_size == vol_size:
 
440
            self.log.debug(msg_attr)
 
441
        else:
 
442
            msg = ('Volume validation failed, {}'.format(msg_attr))
 
443
            amulet.raise_status(amulet.FAIL, msg=msg)
 
444
 
 
445
        return vol_new
 
446
 
 
447
    def delete_resource(self, resource, resource_id,
 
448
                        msg="resource", max_wait=120):
 
449
        """Delete one openstack resource, such as one instance, keypair,
 
450
        image, volume, stack, etc., and confirm deletion within max wait time.
 
451
 
 
452
        :param resource: pointer to os resource type, ex:glance_client.images
 
453
        :param resource_id: unique name or id for the openstack resource
 
454
        :param msg: text to identify purpose in logging
 
455
        :param max_wait: maximum wait time in seconds
 
456
        :returns: True if successful, otherwise False
 
457
        """
 
458
        self.log.debug('Deleting OpenStack resource '
 
459
                       '{} ({})'.format(resource_id, msg))
 
460
        num_before = len(list(resource.list()))
 
461
        resource.delete(resource_id)
 
462
 
 
463
        tries = 0
 
464
        num_after = len(list(resource.list()))
 
465
        while num_after != (num_before - 1) and tries < (max_wait / 4):
 
466
            self.log.debug('{} delete check: '
 
467
                           '{} [{}:{}] {}'.format(msg, tries,
 
468
                                                  num_before,
 
469
                                                  num_after,
 
470
                                                  resource_id))
 
471
            time.sleep(4)
 
472
            num_after = len(list(resource.list()))
 
473
            tries += 1
 
474
 
 
475
        self.log.debug('{}:  expected, actual count = {}, '
 
476
                       '{}'.format(msg, num_before - 1, num_after))
 
477
 
 
478
        if num_after == (num_before - 1):
 
479
            return True
 
480
        else:
 
481
            self.log.error('{} delete timed out'.format(msg))
 
482
            return False
 
483
 
 
484
    def resource_reaches_status(self, resource, resource_id,
 
485
                                expected_stat='available',
 
486
                                msg='resource', max_wait=120):
 
487
        """Wait for an openstack resources status to reach an
 
488
           expected status within a specified time.  Useful to confirm that
 
489
           nova instances, cinder vols, snapshots, glance images, heat stacks
 
490
           and other resources eventually reach the expected status.
 
491
 
 
492
        :param resource: pointer to os resource type, ex: heat_client.stacks
 
493
        :param resource_id: unique id for the openstack resource
 
494
        :param expected_stat: status to expect resource to reach
 
495
        :param msg: text to identify purpose in logging
 
496
        :param max_wait: maximum wait time in seconds
 
497
        :returns: True if successful, False if status is not reached
 
498
        """
 
499
 
 
500
        tries = 0
 
501
        resource_stat = resource.get(resource_id).status
 
502
        while resource_stat != expected_stat and tries < (max_wait / 4):
 
503
            self.log.debug('{} status check: '
 
504
                           '{} [{}:{}] {}'.format(msg, tries,
 
505
                                                  resource_stat,
 
506
                                                  expected_stat,
 
507
                                                  resource_id))
 
508
            time.sleep(4)
 
509
            resource_stat = resource.get(resource_id).status
 
510
            tries += 1
 
511
 
 
512
        self.log.debug('{}:  expected, actual status = {}, '
 
513
                       '{}'.format(msg, resource_stat, expected_stat))
 
514
 
 
515
        if resource_stat == expected_stat:
 
516
            return True
 
517
        else:
 
518
            self.log.debug('{} never reached expected status: '
 
519
                           '{}'.format(resource_id, expected_stat))
 
520
            return False
 
521
 
 
522
    def get_ceph_osd_id_cmd(self, index):
 
523
        """Produce a shell command that will return a ceph-osd id."""
 
524
        return ("`initctl list | grep 'ceph-osd ' | "
 
525
                "awk 'NR=={} {{ print $2 }}' | "
 
526
                "grep -o '[0-9]*'`".format(index + 1))
 
527
 
 
528
    def get_ceph_pools(self, sentry_unit):
 
529
        """Return a dict of ceph pools from a single ceph unit, with
 
530
        pool name as keys, pool id as vals."""
 
531
        pools = {}
 
532
        cmd = 'sudo ceph osd lspools'
 
533
        output, code = sentry_unit.run(cmd)
 
534
        if code != 0:
 
535
            msg = ('{} `{}` returned {} '
 
536
                   '{}'.format(sentry_unit.info['unit_name'],
 
537
                               cmd, code, output))
 
538
            amulet.raise_status(amulet.FAIL, msg=msg)
 
539
 
 
540
        # Example output: 0 data,1 metadata,2 rbd,3 cinder,4 glance,
 
541
        for pool in str(output).split(','):
 
542
            pool_id_name = pool.split(' ')
 
543
            if len(pool_id_name) == 2:
 
544
                pool_id = pool_id_name[0]
 
545
                pool_name = pool_id_name[1]
 
546
                pools[pool_name] = int(pool_id)
 
547
 
 
548
        self.log.debug('Pools on {}: {}'.format(sentry_unit.info['unit_name'],
 
549
                                                pools))
 
550
        return pools
 
551
 
 
552
    def get_ceph_df(self, sentry_unit):
 
553
        """Return dict of ceph df json output, including ceph pool state.
 
554
 
 
555
        :param sentry_unit: Pointer to amulet sentry instance (juju unit)
 
556
        :returns: Dict of ceph df output
 
557
        """
 
558
        cmd = 'sudo ceph df --format=json'
 
559
        output, code = sentry_unit.run(cmd)
 
560
        if code != 0:
 
561
            msg = ('{} `{}` returned {} '
 
562
                   '{}'.format(sentry_unit.info['unit_name'],
 
563
                               cmd, code, output))
 
564
            amulet.raise_status(amulet.FAIL, msg=msg)
 
565
        return json.loads(output)
 
566
 
 
567
    def get_ceph_pool_sample(self, sentry_unit, pool_id=0):
 
568
        """Take a sample of attributes of a ceph pool, returning ceph
 
569
        pool name, object count and disk space used for the specified
 
570
        pool ID number.
 
571
 
 
572
        :param sentry_unit: Pointer to amulet sentry instance (juju unit)
 
573
        :param pool_id: Ceph pool ID
 
574
        :returns: List of pool name, object count, kb disk space used
 
575
        """
 
576
        df = self.get_ceph_df(sentry_unit)
 
577
        pool_name = df['pools'][pool_id]['name']
 
578
        obj_count = df['pools'][pool_id]['stats']['objects']
 
579
        kb_used = df['pools'][pool_id]['stats']['kb_used']
 
580
        self.log.debug('Ceph {} pool (ID {}): {} objects, '
 
581
                       '{} kb used'.format(pool_name, pool_id,
 
582
                                           obj_count, kb_used))
 
583
        return pool_name, obj_count, kb_used
 
584
 
 
585
    def validate_ceph_pool_samples(self, samples, sample_type="resource pool"):
 
586
        """Validate ceph pool samples taken over time, such as pool
 
587
        object counts or pool kb used, before adding, after adding, and
 
588
        after deleting items which affect those pool attributes.  The
 
589
        2nd element is expected to be greater than the 1st; 3rd is expected
 
590
        to be less than the 2nd.
 
591
 
 
592
        :param samples: List containing 3 data samples
 
593
        :param sample_type: String for logging and usage context
 
594
        :returns: None if successful, Failure message otherwise
 
595
        """
 
596
        original, created, deleted = range(3)
 
597
        if samples[created] <= samples[original] or \
 
598
                samples[deleted] >= samples[created]:
 
599
            return ('Ceph {} samples ({}) '
 
600
                    'unexpected.'.format(sample_type, samples))
 
601
        else:
 
602
            self.log.debug('Ceph {} samples (OK): '
 
603
                           '{}'.format(sample_type, samples))
 
604
            return None