171
175
self.log.debug('Checking if tenant exists ({})...'.format(tenant))
172
176
return tenant in [t.name for t in keystone.tenants.list()]
178
def authenticate_cinder_admin(self, keystone_sentry, username,
180
"""Authenticates admin user with cinder."""
181
# NOTE(beisner): cinder python client doesn't accept tokens.
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)
174
188
def authenticate_keystone_admin(self, keystone_sentry, user, password,
176
190
"""Authenticates admin user with the keystone admin endpoint."""
212
226
return nova_client.Client(username=user, api_key=password,
213
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,
215
240
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))
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
218
252
http_proxy = os.getenv('AMULET_HTTP_PROXY')
219
253
self.log.debug('AMULET_HTTP_PROXY: {}'.format(http_proxy))
224
258
opener = urllib.FancyURLopener()
226
f = opener.open("http://download.cirros-cloud.net/version/released")
260
f = opener.open('http://download.cirros-cloud.net/version/released')
227
261
version = f.read().strip()
228
cirros_img = "cirros-{}-x86_64-disk.img".format(version)
262
cirros_img = 'cirros-{}-x86_64-disk.img'.format(version)
229
263
local_path = os.path.join('tests', cirros_img)
231
265
if not os.path.exists(local_path):
232
cirros_url = "http://{}/{}/{}".format("download.cirros-cloud.net",
266
cirros_url = 'http://{}/{}/{}'.format('download.cirros-cloud.net',
233
267
version, cirros_img)
234
268
opener.retrieve(cirros_url, local_path)
271
# Create glance image
237
272
with open(local_path) as f:
238
273
image = glance.images.create(name=image_name, is_public=True,
239
274
disk_format='qcow2',
240
275
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')
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)
260
312
self.log.warn('/!\\ DEPRECATION WARNING: use '
261
313
'delete_resource instead of delete_image.')
262
314
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')
315
return self.delete_resource(glance.images, image, msg='glance image')
280
317
def create_instance(self, nova, image_name, instance_name, flavor):
281
318
"""Create the specified instance."""
308
345
self.log.warn('/!\\ DEPRECATION WARNING: use '
309
346
'delete_resource instead of delete_instance.')
310
347
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')
348
return self.delete_resource(nova.servers, instance,
328
351
def create_or_get_keypair(self, nova, keypair_name="testkey"):
329
352
"""Create a new keypair, or return pointer if it already exists."""
339
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)
342
447
def delete_resource(self, resource, resource_id,
343
448
msg="resource", max_wait=120):
344
449
"""Delete one openstack resource, such as one instance, keypair,
411
518
self.log.debug('{} never reached expected status: '
412
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))