~chris-gondolin/charms/trusty/keystone/ldap-ca-cert

« back to all changes in this revision

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

  • Committer: billy.olsen at canonical
  • Date: 2015-08-31 17:35:57 UTC
  • mfrom: (170.1.39 stable.remote)
  • Revision ID: billy.olsen@canonical.com-20150831173557-0r0ftkapbitq0s20
[ack,r=billy-olsen,1chb1n,tealeg,adam-collard] Add pause/resume actions to keystone.

This changes introduces the pause and resume action set to the keystone charm. These
actions can be used to pause keystone services on a unit for maintenance activities.

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