183
207
def authenticate_glance_admin(self, keystone):
184
208
"""Authenticates admin user with glance."""
209
self.log.debug('Authenticating glance admin...')
185
210
ep = keystone.service_catalog.url_for(service_type='image',
186
211
endpoint_type='adminURL')
187
212
return glance_client.Client(ep, token=keystone.auth_token)
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)
189
221
def authenticate_nova_user(self, keystone, user, password, tenant):
190
222
"""Authenticates a regular user with nova-api."""
223
self.log.debug('Authenticating nova user ({})...'.format(user))
191
224
ep = keystone.service_catalog.url_for(service_type='identity',
192
225
endpoint_type='publicURL')
193
226
return nova_client.Client(username=user, api_key=password,
194
227
project_id=tenant, auth_url=ep)
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,
196
240
def create_cirros_image(self, glance, image_name):
197
"""Download the latest cirros image and upload it to glance."""
241
"""Download the latest cirros image and upload it to glance,
242
validate and return a resource pointer.
244
:param glance: pointer to authenticated glance connection
245
:param image_name: display name for new image
246
:returns: glance image pointer
248
self.log.debug('Creating glance cirros image '
249
'({})...'.format(image_name))
251
# Download cirros image
198
252
http_proxy = os.getenv('AMULET_HTTP_PROXY')
199
253
self.log.debug('AMULET_HTTP_PROXY: {}'.format(http_proxy))
204
258
opener = urllib.FancyURLopener()
206
f = opener.open("http://download.cirros-cloud.net/version/released")
260
f = opener.open('http://download.cirros-cloud.net/version/released')
207
261
version = f.read().strip()
208
cirros_img = "cirros-{}-x86_64-disk.img".format(version)
262
cirros_img = 'cirros-{}-x86_64-disk.img'.format(version)
209
263
local_path = os.path.join('tests', cirros_img)
211
265
if not os.path.exists(local_path):
212
cirros_url = "http://{}/{}/{}".format("download.cirros-cloud.net",
266
cirros_url = 'http://{}/{}/{}'.format('download.cirros-cloud.net',
213
267
version, cirros_img)
214
268
opener.retrieve(cirros_url, local_path)
271
# Create glance image
217
272
with open(local_path) as f:
218
273
image = glance.images.create(name=image_name, is_public=True,
219
274
disk_format='qcow2',
220
275
container_format='bare', data=f)
222
status = image.status
223
while status != 'active' and count < 10:
225
image = glance.images.get(image.id)
226
status = image.status
227
self.log.debug('image status: {}'.format(status))
230
if status != 'active':
231
self.log.error('image creation timed out')
277
# Wait for image to reach active status
279
ret = self.resource_reaches_status(glance.images, img_id,
280
expected_stat='active',
281
msg='Image status wait')
283
msg = 'Glance image failed to reach expected state.'
284
amulet.raise_status(amulet.FAIL, msg=msg)
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))
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)
303
msg = ('Volume validation failed, {}'.format(msg_attr))
304
amulet.raise_status(amulet.FAIL, msg=msg)
236
308
def delete_image(self, glance, image):
237
309
"""Delete the specified image."""
238
num_before = len(list(glance.images.list()))
239
glance.images.delete(image)
242
num_after = len(list(glance.images.list()))
243
while num_after != (num_before - 1) and count < 10:
245
num_after = len(list(glance.images.list()))
246
self.log.debug('number of images: {}'.format(num_after))
249
if num_after != (num_before - 1):
250
self.log.error('image deletion timed out')
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')
255
317
def create_instance(self, nova, image_name, instance_name, flavor):
256
318
"""Create the specified instance."""
319
self.log.debug('Creating instance '
320
'({}|{}|{})'.format(instance_name, image_name, flavor))
257
321
image = nova.images.find(name=image_name)
258
322
flavor = nova.flavors.find(name=flavor)
259
323
instance = nova.servers.create(name=instance_name, image=image,
277
341
def delete_instance(self, nova, instance):
278
342
"""Delete the specified instance."""
279
num_before = len(list(nova.servers.list()))
280
nova.servers.delete(instance)
283
num_after = len(list(nova.servers.list()))
284
while num_after != (num_before - 1) and count < 10:
286
num_after = len(list(nova.servers.list()))
287
self.log.debug('number of instances: {}'.format(num_after))
290
if num_after != (num_before - 1):
291
self.log.error('instance deletion timed out')
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,
351
def create_or_get_keypair(self, nova, keypair_name="testkey"):
352
"""Create a new keypair, or return pointer if it already exists."""
354
_keypair = nova.keypairs.get(keypair_name)
355
self.log.debug('Keypair ({}) already exists, '
356
'using it.'.format(keypair_name))
359
self.log.debug('Keypair ({}) does not exist, '
360
'creating it.'.format(keypair_name))
362
_keypair = nova.keypairs.create(name=keypair_name)
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.
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
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...')
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)
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:
397
self.log.debug('Creating cinder volume...')
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,
405
amulet.raise_status(amulet.FAIL, msg=msg)
409
vol_new = cinder.volumes.create(display_name=vol_name,
412
source_volid=src_vol_id,
415
except Exception as e:
416
msg = 'Failed to create volume: {}'.format(e)
417
amulet.raise_status(amulet.FAIL, msg=msg)
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")
424
msg = 'Cinder volume failed to reach expected state.'
425
amulet.raise_status(amulet.FAIL, msg=msg)
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,
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)
442
msg = ('Volume validation failed, {}'.format(msg_attr))
443
amulet.raise_status(amulet.FAIL, msg=msg)
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.
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
458
self.log.debug('Deleting OpenStack resource '
459
'{} ({})'.format(resource_id, msg))
460
num_before = len(list(resource.list()))
461
resource.delete(resource_id)
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,
472
num_after = len(list(resource.list()))
475
self.log.debug('{}: expected, actual count = {}, '
476
'{}'.format(msg, num_before - 1, num_after))
478
if num_after == (num_before - 1):
481
self.log.error('{} delete timed out'.format(msg))
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.
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
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,
509
resource_stat = resource.get(resource_id).status
512
self.log.debug('{}: expected, actual status = {}, '
513
'{}'.format(msg, resource_stat, expected_stat))
515
if resource_stat == expected_stat:
518
self.log.debug('{} never reached expected status: '
519
'{}'.format(resource_id, expected_stat))
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))
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."""
532
cmd = 'sudo ceph osd lspools'
533
output, code = sentry_unit.run(cmd)
535
msg = ('{} `{}` returned {} '
536
'{}'.format(sentry_unit.info['unit_name'],
538
amulet.raise_status(amulet.FAIL, msg=msg)
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)
548
self.log.debug('Pools on {}: {}'.format(sentry_unit.info['unit_name'],
552
def get_ceph_df(self, sentry_unit):
553
"""Return dict of ceph df json output, including ceph pool state.
555
:param sentry_unit: Pointer to amulet sentry instance (juju unit)
556
:returns: Dict of ceph df output
558
cmd = 'sudo ceph df --format=json'
559
output, code = sentry_unit.run(cmd)
561
msg = ('{} `{}` returned {} '
562
'{}'.format(sentry_unit.info['unit_name'],
564
amulet.raise_status(amulet.FAIL, msg=msg)
565
return json.loads(output)
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
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
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,
583
return pool_name, obj_count, kb_used
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.
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
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))
602
self.log.debug('Ceph {} samples (OK): '
603
'{}'.format(sample_type, samples))