171
174
self.log.debug('Checking if tenant exists ({})...'.format(tenant))
172
175
return tenant in [t.name for t in keystone.tenants.list()]
177
def authenticate_cinder_admin(self, keystone_sentry, username,
179
"""Authenticates admin user with cinder."""
181
keystone_sentry.relation('shared-db',
182
'mysql:shared-db')['private-address']
183
ept = "http://{}:5000/v2.0".format(service_ip.strip().decode('utf-8'))
184
return cinder_client.Client(username, password, tenant, ept)
174
186
def authenticate_keystone_admin(self, keystone_sentry, user, password,
176
188
"""Authenticates admin user with the keystone admin endpoint."""
212
224
return nova_client.Client(username=user, api_key=password,
213
225
project_id=tenant, auth_url=ep)
227
def authenticate_swift_user(self, keystone, user, password, tenant):
228
"""Authenticates a regular user with swift api."""
229
self.log.debug('Authenticating swift user ({})...'.format(user))
230
ep = keystone.service_catalog.url_for(service_type='identity',
231
endpoint_type='publicURL')
232
return swiftclient.Connection(authurl=ep,
215
238
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))
239
"""Download the latest cirros image and upload it to glance,
240
validate and return a resource pointer.
242
:param glance: pointer to authenticated glance connection
243
:param image_name: display name for new image
244
:returns: glance image pointer
246
self.log.debug('Creating glance cirros image '
247
'({})...'.format(image_name))
249
# Download cirros image
218
250
http_proxy = os.getenv('AMULET_HTTP_PROXY')
219
251
self.log.debug('AMULET_HTTP_PROXY: {}'.format(http_proxy))
224
256
opener = urllib.FancyURLopener()
226
f = opener.open("http://download.cirros-cloud.net/version/released")
258
f = opener.open('http://download.cirros-cloud.net/version/released')
227
259
version = f.read().strip()
228
cirros_img = "cirros-{}-x86_64-disk.img".format(version)
260
cirros_img = 'cirros-{}-x86_64-disk.img'.format(version)
229
261
local_path = os.path.join('tests', cirros_img)
231
263
if not os.path.exists(local_path):
232
cirros_url = "http://{}/{}/{}".format("download.cirros-cloud.net",
264
cirros_url = 'http://{}/{}/{}'.format('download.cirros-cloud.net',
233
265
version, cirros_img)
234
266
opener.retrieve(cirros_url, local_path)
269
# Create glance image
237
270
with open(local_path) as f:
238
271
image = glance.images.create(name=image_name, is_public=True,
239
272
disk_format='qcow2',
240
273
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')
275
# Wait for image to reach active status
277
ret = self.resource_reaches_status(glance.images, img_id,
278
expected_stat='active',
279
msg='Image status wait')
281
msg = 'Glance image failed to reach expected state.'
282
raise RuntimeError(msg)
284
# Re-validate new image
285
self.log.debug('Validating image attributes...')
286
val_img_name = glance.images.get(img_id).name
287
val_img_stat = glance.images.get(img_id).status
288
val_img_pub = glance.images.get(img_id).is_public
289
val_img_cfmt = glance.images.get(img_id).container_format
290
val_img_dfmt = glance.images.get(img_id).disk_format
291
msg_attr = ('Image attributes - name:{} public:{} id:{} stat:{} '
292
'container fmt:{} disk fmt:{}'.format(
293
val_img_name, val_img_pub, img_id,
294
val_img_stat, val_img_cfmt, val_img_dfmt))
296
if val_img_name == image_name and val_img_stat == 'active' \
297
and val_img_pub is True and val_img_cfmt == 'bare' \
298
and val_img_dfmt == 'qcow2':
299
self.log.debug(msg_attr)
301
msg = ('Volume validation failed, {}'.format(msg_attr))
302
raise RuntimeError(msg)
260
310
self.log.warn('/!\\ DEPRECATION WARNING: use '
261
311
'delete_resource instead of delete_image.')
262
312
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')
313
return self.delete_resource(glance.images, image, msg='glance image')
280
315
def create_instance(self, nova, image_name, instance_name, flavor):
281
316
"""Create the specified instance."""
308
343
self.log.warn('/!\\ DEPRECATION WARNING: use '
309
344
'delete_resource instead of delete_instance.')
310
345
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')
346
return self.delete_resource(nova.servers, instance,
328
349
def create_or_get_keypair(self, nova, keypair_name="testkey"):
329
350
"""Create a new keypair, or return pointer if it already exists."""
339
360
_keypair = nova.keypairs.create(name=keypair_name)
363
def create_cinder_volume(self, cinder, vol_name="demo-vol", vol_size=1,
364
img_id=None, src_vol_id=None, snap_id=None):
365
"""Create cinder volume, optionally from a glance image, or
366
optionally as a clone of an existing volume, or optionally
367
from a snapshot. Wait for the new volume status to reach
368
the expected status, validate and return a resource pointer.
370
:param vol_name: cinder volume display name
371
:param vol_size: size in gigabytes
372
:param img_id: optional glance image id
373
:param src_vol_id: optional source volume id to clone
374
:param snap_id: optional snapshot id to use
375
:returns: cinder volume pointer
377
# Handle parameter input
378
if img_id and not src_vol_id and not snap_id:
379
self.log.debug('Creating cinder volume from glance image '
380
'({})...'.format(img_id))
382
elif src_vol_id and not img_id and not snap_id:
383
self.log.debug('Cloning cinder volume...')
384
bootable = cinder.volumes.get(src_vol_id).bootable
385
elif snap_id and not src_vol_id and not img_id:
386
self.log.debug('Creating cinder volume from snapshot...')
387
snap = cinder.volume_snapshots.find(id=snap_id)
389
snap_vol_id = cinder.volume_snapshots.get(snap_id).volume_id
390
bootable = cinder.volumes.get(snap_vol_id).bootable
391
elif not img_id and not src_vol_id and not snap_id:
392
self.log.debug('Creating cinder volume...')
395
msg = ('Invalid method use - name:{} size:{} img_id:{} '
396
'src_vol_id:{} snap_id:{}'.format(vol_name, vol_size,
399
raise RuntimeError(msg)
403
vol_new = cinder.volumes.create(display_name=vol_name,
406
source_volid=src_vol_id,
409
except Exception as e:
410
msg = 'Failed to create volume: {}'.format(e)
411
raise RuntimeError(msg)
413
# Wait for volume to reach available status
414
ret = self.resource_reaches_status(cinder.volumes, vol_id,
415
expected_stat="available",
416
msg="Volume status wait")
418
msg = 'Cinder volume failed to reach expected state.'
419
raise RuntimeError(msg)
421
# Re-validate new volume
422
self.log.debug('Validating volume attributes...')
423
val_vol_name = cinder.volumes.get(vol_id).display_name
424
val_vol_boot = cinder.volumes.get(vol_id).bootable
425
val_vol_stat = cinder.volumes.get(vol_id).status
426
val_vol_size = cinder.volumes.get(vol_id).size
427
msg_attr = ('Volume attributes - name:{} id:{} stat:{} boot:'
428
'{} size:{}'.format(val_vol_name, vol_id,
429
val_vol_stat, val_vol_boot,
432
if val_vol_boot == bootable and val_vol_stat == 'available' \
433
and val_vol_name == vol_name and val_vol_size == vol_size:
434
self.log.debug(msg_attr)
436
msg = ('Volume validation failed, {}'.format(msg_attr))
437
raise RuntimeError(msg)
342
441
def delete_resource(self, resource, resource_id,
343
442
msg="resource", max_wait=120):
344
443
"""Delete one openstack resource, such as one instance, keypair,
411
512
self.log.debug('{} never reached expected status: '
412
513
'{}'.format(resource_id, expected_stat))
516
def get_ceph_osd_id_cmd(self, index):
517
"""Produce a shell command that will return a ceph-osd id."""
518
cmd = ("`initctl list | grep 'ceph-osd ' | awk 'NR=={} {{ print $2 }}'"
519
" | grep -o '[0-9]*'`".format(index + 1))
522
def get_ceph_pools(self, sentry_unit):
523
"""Return a dict of ceph pools from a single ceph unit, with
524
pool name as keys, pool id as vals."""
526
cmd = 'sudo ceph osd lspools'
527
output, code = sentry_unit.run(cmd)
529
msg = ('{} `{}` returned {} '
530
'{}'.format(sentry_unit.info['unit_name'],
532
raise RuntimeError(msg)
534
# Example output: 0 data,1 metadata,2 rbd,3 cinder,4 glance,
535
for pool in str(output).split(','):
536
pool_id_name = pool.split(' ')
537
if len(pool_id_name) == 2:
538
pool_id = pool_id_name[0]
539
pool_name = pool_id_name[1]
540
pools[pool_name] = int(pool_id)
542
self.log.debug('Pools on {}: {}'.format(sentry_unit.info['unit_name'],
546
def get_ceph_df(self, sentry_unit):
547
"""Return dict of ceph df json output, including ceph pool state.
549
:param sentry_unit: Pointer to amulet sentry instance (juju unit)
550
:returns: Dict of ceph df output
552
cmd = 'sudo ceph df --format=json'
553
output, code = sentry_unit.run(cmd)
555
msg = ('{} `{}` returned {} '
556
'{}'.format(sentry_unit.info['unit_name'],
558
raise RuntimeError(msg)
559
return json.loads(output)
561
def get_ceph_pool_sample(self, sentry_unit, pool_id=0):
562
"""Take a sample of attributes of a ceph pool, returning ceph
563
pool name, object count and disk space used for the specified
566
:param sentry_unit: Pointer to amulet sentry instance (juju unit)
567
:param pool_id: Ceph pool ID
568
:returns: List of pool name, object count, kb disk space used
570
df = self.get_ceph_df(sentry_unit)
571
pool_name = df['pools'][pool_id]['name']
572
obj_count = df['pools'][pool_id]['stats']['objects']
573
kb_used = df['pools'][pool_id]['stats']['kb_used']
574
self.log.debug('Ceph {} pool (ID {}): {} objects, '
575
'{} kb used'.format(pool_name,
579
return pool_name, obj_count, kb_used
581
def validate_ceph_pool_samples(self, samples, sample_type="resource pool"):
582
"""Validate ceph pool samples taken over time, such as pool
583
object counts or pool kb used, before adding, after adding, and
584
after deleting items which affect those pool attributes. The
585
2nd element is expected to be greater than the 1st; 3rd is expected
586
to be less than the 2nd.
588
:param samples: List containing 3 data samples
589
:param sample_type: String for logging and usage context
590
:returns: None if successful, Failure message otherwise
592
original, created, deleted = range(3)
593
if samples[created] <= samples[original] or \
594
samples[deleted] >= samples[created]:
595
msg = ('Ceph {} samples ({}) '
596
'unexpected.'.format(sample_type, samples))
599
self.log.debug('Ceph {} samples (OK): '
600
'{}'.format(sample_type, samples))