785 lines
33 KiB
Python
Executable File
785 lines
33 KiB
Python
Executable File
#!/usr/bin/python
|
|
|
|
import amulet
|
|
import types
|
|
from time import sleep
|
|
import cinderclient.v1.client as cinder_client
|
|
|
|
from charmhelpers.contrib.openstack.amulet.deployment import (
|
|
OpenStackAmuletDeployment
|
|
)
|
|
|
|
from charmhelpers.contrib.openstack.amulet.utils import ( # noqa
|
|
OpenStackAmuletUtils,
|
|
DEBUG,
|
|
ERROR
|
|
)
|
|
|
|
# Use DEBUG to turn on debug logging
|
|
u = OpenStackAmuletUtils(ERROR)
|
|
|
|
|
|
class CinderBasicDeployment(OpenStackAmuletDeployment):
|
|
'''Amulet tests on a basic lvm-backed cinder deployment. Verify
|
|
relations, service status, users and endpoint service catalog.
|
|
Create, clone, delete volumes. Create volume from glance image.
|
|
Create volume snapshot. Create volume from snapshot.'''
|
|
|
|
# NOTE(beisner): Features and tests vary across Openstack releases.
|
|
# https://wiki.openstack.org/wiki/CinderSupportMatrix
|
|
|
|
def __init__(self, series=None, openstack=None, source=None, stable=False):
|
|
'''Deploy the entire test environment.'''
|
|
super(CinderBasicDeployment, self).__init__(series, openstack, source,
|
|
stable)
|
|
self._add_services()
|
|
self._add_relations()
|
|
self._configure_services()
|
|
self._deploy()
|
|
self._initialize_tests()
|
|
|
|
def _add_services(self):
|
|
"""Add services
|
|
|
|
Add the services that we're testing, where cinder is local,
|
|
and the rest of the service are from lp branches that are
|
|
compatible with the local charm (e.g. stable or next).
|
|
"""
|
|
this_service = {'name': 'cinder'}
|
|
other_services = [{'name': 'mysql'}, {'name': 'rabbitmq-server'},
|
|
{'name': 'keystone'}, {'name': 'glance'}]
|
|
super(CinderBasicDeployment, self)._add_services(this_service,
|
|
other_services)
|
|
|
|
def _add_relations(self):
|
|
'''Add relations for the services.'''
|
|
relations = {
|
|
'keystone:shared-db': 'mysql:shared-db',
|
|
'cinder:shared-db': 'mysql:shared-db',
|
|
'cinder:identity-service': 'keystone:identity-service',
|
|
'cinder:amqp': 'rabbitmq-server:amqp',
|
|
'cinder:image-service': 'glance:image-service',
|
|
'glance:identity-service': 'keystone:identity-service',
|
|
'glance:shared-db': 'mysql:shared-db',
|
|
'glance:amqp': 'rabbitmq-server:amqp'
|
|
}
|
|
super(CinderBasicDeployment, self)._add_relations(relations)
|
|
|
|
def _configure_services(self):
|
|
'''Configure all of the services.'''
|
|
keystone_config = {'admin-password': 'openstack',
|
|
'admin-token': 'ubuntutesting'}
|
|
cinder_config = {'block-device': 'vdb',
|
|
'glance-api-version': '2',
|
|
'overwrite': 'true'}
|
|
mysql_config = {'dataset-size': '50%'}
|
|
configs = {'cinder': cinder_config,
|
|
'keystone': keystone_config,
|
|
'mysql': mysql_config}
|
|
super(CinderBasicDeployment, self)._configure_services(configs)
|
|
|
|
def _initialize_tests(self):
|
|
'''Perform final initialization before tests get run.'''
|
|
# Access the sentries for inspecting service units
|
|
self.cinder_sentry = self.d.sentry.unit['cinder/0']
|
|
self.glance_sentry = self.d.sentry.unit['glance/0']
|
|
self.mysql_sentry = self.d.sentry.unit['mysql/0']
|
|
self.keystone_sentry = self.d.sentry.unit['keystone/0']
|
|
self.rabbitmq_sentry = self.d.sentry.unit['rabbitmq-server/0']
|
|
|
|
# Authenticate admin with keystone
|
|
self.keystone = u.authenticate_keystone_admin(self.keystone_sentry,
|
|
user='admin',
|
|
password='openstack',
|
|
tenant='admin')
|
|
# Authenticate admin with cinder endpoint
|
|
self.cinder = self.authenticate_cinder_admin(username='admin',
|
|
password='openstack',
|
|
tenant='admin')
|
|
# Authenticate admin with glance endpoint
|
|
self.glance = u.authenticate_glance_admin(self.keystone)
|
|
|
|
u.log.debug('openstack rel: {}'.format(self._get_openstack_release()))
|
|
# Wait for relations to settle
|
|
sleep(120)
|
|
|
|
def authenticate_cinder_admin(self, username, password, tenant):
|
|
"""Authenticates admin user with cinder."""
|
|
# NOTE(beisner): need to move to charmhelpers, and adjust calls here.
|
|
# Probably useful on other charm tests.
|
|
service_ip = \
|
|
self.keystone_sentry.relation('shared-db',
|
|
'mysql:shared-db')['private-address']
|
|
ept = "http://{}:5000/v2.0".format(service_ip.strip().decode('utf-8'))
|
|
return cinder_client.Client(username, password, tenant, ept)
|
|
|
|
def force_list(self, obj):
|
|
'''Determine the object type and return a list. Some Openstack
|
|
component API list methods return generators, some return lists.
|
|
Where obj is cinder.volumes, cinder.volume_snapshots, glance.images,
|
|
or other Openstack object with a list method.'''
|
|
# NOTE(beisner): need to move to charmhelpers, and adjust calls here.
|
|
|
|
# NOTE(beisner): Beware - glance's list method returns a generator,
|
|
# and cinder's list method returns a list!
|
|
if isinstance(obj.list(), types.ListType):
|
|
return obj.list()
|
|
elif isinstance(obj.list(), types.GeneratorType):
|
|
return list(obj.list())
|
|
else:
|
|
u.log.debug('unhandled object type: {}'.format(type(obj.list())))
|
|
return False
|
|
|
|
def delete_all_objs(self, obj, item_desc='object', max_wait=60):
|
|
'''Delete all objects from openstack component, such as all volumes,
|
|
all images or all snapshots. Waits and confirms deletion.'''
|
|
# NOTE(beisner): need to move to charmhelpers, and adjust calls here.
|
|
# Probably useful on other charm tests.
|
|
|
|
# Get list of objects to delete
|
|
obj_list = self.force_list(obj)
|
|
if obj_list is False:
|
|
return '{} list failed'.format(item_desc)
|
|
|
|
if len(obj_list) == 0:
|
|
u.log.debug('no {}(s) to delete'.format(item_desc))
|
|
return None
|
|
|
|
# Delete objects
|
|
for obj_this in obj_list:
|
|
u.log.debug('deleting {}: {}'.format(item_desc, obj_this.id))
|
|
try:
|
|
obj_this.delete()
|
|
except:
|
|
return '{} delete failed for {} with status {}'.format(
|
|
item_desc, obj_this.id, obj_this.status)
|
|
|
|
# Wait for objects to disappear
|
|
obj_count = len(self.force_list(obj))
|
|
tries = 0
|
|
while obj_count != 0 and tries <= (max_wait/4):
|
|
u.log.debug('{} delete wait: {} {}'.format(item_desc,
|
|
tries, obj_count))
|
|
sleep(4)
|
|
obj_count = len(self.force_list(obj))
|
|
tries += 1
|
|
|
|
if obj_count != 0:
|
|
return '{}(s) not deleted, {} remain.'.format(item_desc,
|
|
obj_count)
|
|
|
|
def obj_is_status(self, obj, obj_id, stat='available',
|
|
msg='openstack object status check', max_wait=60):
|
|
''''Wait for an openstack object status to be as expected.
|
|
By default, expect an available status within 60s. Useful
|
|
when confirming cinder volumes, snapshots, glance images, etc.
|
|
reach a certain state/status within a specified time.'''
|
|
# NOTE(beisner): need to move to charmhelpers, and adjust calls here.
|
|
# Probably useful on other charm tests.
|
|
|
|
obj_stat = obj.get(obj_id).status
|
|
tries = 0
|
|
while obj_stat != stat and tries < (max_wait/4):
|
|
u.log.debug(msg + ': {} [{}:{}] {}'.format(tries, obj_stat,
|
|
stat, obj_id))
|
|
sleep(4)
|
|
obj_stat = obj.get(obj_id).status
|
|
tries += 1
|
|
if obj_stat == stat:
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
def test_services(self):
|
|
'''Verify that the expected services are running on the
|
|
corresponding service units.'''
|
|
commands = {
|
|
self.cinder_sentry: ['status cinder-api',
|
|
'status cinder-scheduler',
|
|
'status cinder-volume'],
|
|
self.glance_sentry: ['status glance-registry',
|
|
'status glance-api'],
|
|
self.mysql_sentry: ['status mysql'],
|
|
self.keystone_sentry: ['status keystone'],
|
|
self.rabbitmq_sentry: ['sudo service rabbitmq-server status']
|
|
}
|
|
u.log.debug('commands: {}'.format(commands))
|
|
ret = u.validate_services(commands)
|
|
if ret:
|
|
amulet.raise_status(amulet.FAIL, msg=ret)
|
|
|
|
def test_service_catalog(self):
|
|
'''Verify that the service catalog endpoint data'''
|
|
endpoint_vol = {'adminURL': u.valid_url,
|
|
'region': 'RegionOne',
|
|
'publicURL': u.valid_url,
|
|
'internalURL': u.valid_url}
|
|
endpoint_id = {'adminURL': u.valid_url,
|
|
'region': 'RegionOne',
|
|
'publicURL': u.valid_url,
|
|
'internalURL': u.valid_url}
|
|
if self._get_openstack_release() >= self.trusty_icehouse:
|
|
endpoint_vol['id'] = u.not_null
|
|
endpoint_id['id'] = u.not_null
|
|
|
|
expected = {'image': [endpoint_id],
|
|
'identity': [endpoint_id],
|
|
'volume': [endpoint_id]}
|
|
actual = self.keystone.service_catalog.get_endpoints()
|
|
|
|
ret = u.validate_svc_catalog_endpoint_data(expected, actual)
|
|
if ret:
|
|
amulet.raise_status(amulet.FAIL, msg=ret)
|
|
|
|
def test_cinder_glance_image_service_relation(self):
|
|
'''Verify the cinder:glance image-service relation data'''
|
|
unit = self.cinder_sentry
|
|
relation = ['image-service', 'glance:image-service']
|
|
expected = {'private-address': u.valid_ip}
|
|
u.log.debug('')
|
|
ret = u.validate_relation_data(unit, relation, expected)
|
|
if ret:
|
|
msg = u.relation_error('cinder image-service', ret)
|
|
amulet.raise_status(amulet.FAIL, msg=msg)
|
|
|
|
def test_glance_cinder_image_service_relation(self):
|
|
'''Verify the glance:cinder image-service relation data'''
|
|
unit = self.glance_sentry
|
|
relation = ['image-service', 'cinder:image-service']
|
|
expected = {
|
|
'private-address': u.valid_ip,
|
|
'glance-api-server': u.valid_url
|
|
}
|
|
u.log.debug('')
|
|
ret = u.validate_relation_data(unit, relation, expected)
|
|
if ret:
|
|
msg = u.relation_error('glance image-service', ret)
|
|
amulet.raise_status(amulet.FAIL, msg=msg)
|
|
|
|
def test_mysql_cinder_db_relation(self):
|
|
'''Verify the mysql:glance shared-db relation data'''
|
|
unit = self.mysql_sentry
|
|
relation = ['shared-db', 'cinder:shared-db']
|
|
expected = {
|
|
'private-address': u.valid_ip,
|
|
'db_host': u.valid_ip
|
|
}
|
|
u.log.debug('')
|
|
ret = u.validate_relation_data(unit, relation, expected)
|
|
if ret:
|
|
msg = u.relation_error('mysql shared-db', ret)
|
|
amulet.raise_status(amulet.FAIL, msg=msg)
|
|
|
|
def test_cinder_mysql_db_relation(self):
|
|
'''Verify the cinder:mysql shared-db relation data'''
|
|
unit = self.cinder_sentry
|
|
relation = ['shared-db', 'mysql:shared-db']
|
|
expected = {
|
|
'private-address': u.valid_ip,
|
|
'hostname': u.valid_ip,
|
|
'username': 'cinder',
|
|
'database': 'cinder'
|
|
}
|
|
u.log.debug('')
|
|
ret = u.validate_relation_data(unit, relation, expected)
|
|
if ret:
|
|
msg = u.relation_error('cinder shared-db', ret)
|
|
amulet.raise_status(amulet.FAIL, msg=msg)
|
|
|
|
def test_keystone_cinder_id_relation(self):
|
|
'''Verify the keystone:cinder identity-service relation data'''
|
|
unit = self.keystone_sentry
|
|
relation = ['identity-service',
|
|
'cinder:identity-service']
|
|
expected = {
|
|
'service_protocol': 'http',
|
|
'service_tenant': 'services',
|
|
'admin_token': 'ubuntutesting',
|
|
'service_password': u.not_null,
|
|
'service_port': '5000',
|
|
'auth_port': '35357',
|
|
'auth_protocol': 'http',
|
|
'private-address': u.valid_ip,
|
|
'https_keystone': 'False',
|
|
'auth_host': u.valid_ip,
|
|
'service_username': 'cinder',
|
|
'service_tenant_id': u.not_null,
|
|
'service_host': u.valid_ip
|
|
}
|
|
u.log.debug('')
|
|
ret = u.validate_relation_data(unit, relation, expected)
|
|
if ret:
|
|
msg = u.relation_error('identity-service cinder', ret)
|
|
amulet.raise_status(amulet.FAIL, msg=msg)
|
|
|
|
def test_cinder_keystone_id_relation(self):
|
|
'''Verify the cinder:keystone identity-service relation data'''
|
|
unit = self.cinder_sentry
|
|
relation = ['identity-service',
|
|
'keystone:identity-service']
|
|
expected = {
|
|
'service': 'cinder',
|
|
'region': 'RegionOne',
|
|
'public_url': u.valid_url,
|
|
'internal_url': u.valid_url,
|
|
'admin_url': u.valid_url,
|
|
'private-address': u.valid_ip
|
|
}
|
|
u.log.debug('')
|
|
ret = u.validate_relation_data(unit, relation, expected)
|
|
if ret:
|
|
msg = u.relation_error('cinder identity-service', ret)
|
|
amulet.raise_status(amulet.FAIL, msg=msg)
|
|
|
|
def test_rabbitmq_cinder_amqp_relation(self):
|
|
'''Verify the rabbitmq-server:cinder amqp relation data'''
|
|
unit = self.rabbitmq_sentry
|
|
relation = ['amqp', 'cinder:amqp']
|
|
expected = {
|
|
'private-address': u.valid_ip,
|
|
'password': u.not_null,
|
|
'hostname': u.valid_ip
|
|
}
|
|
u.log.debug('')
|
|
ret = u.validate_relation_data(unit, relation, expected)
|
|
if ret:
|
|
msg = u.relation_error('amqp cinder', ret)
|
|
amulet.raise_status(amulet.FAIL, msg=msg)
|
|
|
|
def test_cinder_rabbitmq_amqp_relation(self):
|
|
'''Verify the cinder:rabbitmq-server amqp relation data'''
|
|
unit = self.cinder_sentry
|
|
relation = ['amqp', 'rabbitmq-server:amqp']
|
|
expected = {
|
|
'private-address': u.valid_ip,
|
|
'vhost': 'openstack',
|
|
'username': u.not_null
|
|
}
|
|
u.log.debug('')
|
|
ret = u.validate_relation_data(unit, relation, expected)
|
|
if ret:
|
|
msg = u.relation_error('cinder amqp', ret)
|
|
amulet.raise_status(amulet.FAIL, msg=msg)
|
|
|
|
def test_cinder_default_config(self):
|
|
'''Verify default section configs in cinder.conf and
|
|
compare some of the parameters to relation data.'''
|
|
unit_ci = self.cinder_sentry
|
|
unit_mq = self.rabbitmq_sentry
|
|
rel_ci_mq = unit_ci.relation('amqp', 'rabbitmq-server:amqp')
|
|
rel_mq_ci = unit_mq.relation('amqp', 'cinder:amqp')
|
|
u.log.debug('actual ci:mq relation: {}'.format(rel_ci_mq))
|
|
u.log.debug('actual mq:ci relation: {}'.format(rel_mq_ci))
|
|
conf = '/etc/cinder/cinder.conf'
|
|
expected = {'use_syslog': 'False',
|
|
'debug': 'False',
|
|
'verbose': 'False',
|
|
'iscsi_helper': 'tgtadm',
|
|
'volume_group': 'cinder-volumes',
|
|
'rabbit_userid': 'cinder',
|
|
'rabbit_password': rel_mq_ci['password'],
|
|
'rabbit_host': rel_mq_ci['hostname'],
|
|
'auth_strategy': 'keystone',
|
|
'volumes_dir': '/var/lib/cinder/volumes'}
|
|
section = 'DEFAULT'
|
|
u.log.debug('')
|
|
ret = u.validate_config_data(unit_ci, conf, section, expected)
|
|
if ret:
|
|
msg = 'cinder.conf default config error: {}'.format(ret)
|
|
amulet.raise_status(amulet.FAIL, msg=msg)
|
|
|
|
def test_cinder_auth_config(self):
|
|
'''Verify authtoken section config in cinder.conf or
|
|
api-paste.ini using glance/keystone relation data.'''
|
|
unit_ci = self.cinder_sentry
|
|
unit_ks = self.keystone_sentry
|
|
rel_ks_ci = unit_ks.relation('identity-service',
|
|
'cinder:identity-service')
|
|
u.log.debug('actual ks:ci relation: {}'.format(rel_ks_ci))
|
|
|
|
expected = {'admin_user': rel_ks_ci['service_username'],
|
|
'admin_password': rel_ks_ci['service_password'],
|
|
'admin_tenant_name': rel_ks_ci['service_tenant'],
|
|
'auth_host': rel_ks_ci['auth_host']}
|
|
|
|
if self._get_openstack_release() >= self.precise_icehouse:
|
|
conf = '/etc/cinder/cinder.conf'
|
|
section = 'keystone_authtoken'
|
|
auth_uri = 'http://' + rel_ks_ci['auth_host'] + \
|
|
':' + rel_ks_ci['service_port'] + '/'
|
|
expected['auth_uri'] = auth_uri
|
|
else:
|
|
conf = '/etc/cinder/api-paste.ini'
|
|
section = 'filter:authtoken'
|
|
|
|
ret = u.validate_config_data(unit_ci, conf, section, expected)
|
|
if ret:
|
|
msg = "cinder auth config error: {}".format(ret)
|
|
amulet.raise_status(amulet.FAIL, msg=msg)
|
|
|
|
def test_cinder_logging_config(self):
|
|
''' Inspect select sections and config pairs in logging.conf.'''
|
|
unit_ci = self.cinder_sentry
|
|
conf = '/etc/cinder/logging.conf'
|
|
|
|
expected = {
|
|
'loggers': {
|
|
'keys': 'root, cinder'
|
|
},
|
|
'logger_cinder': {
|
|
'level': 'INFO',
|
|
'handlers': 'stderr',
|
|
'qualname': 'cinder'
|
|
},
|
|
'logger_root': {
|
|
'level': 'WARNING',
|
|
'handlers': 'null'
|
|
}
|
|
}
|
|
|
|
for section, pairs in expected.iteritems():
|
|
ret = u.validate_config_data(unit_ci, conf, section, pairs)
|
|
if ret:
|
|
msg = "cinder logging config error: {}".format(ret)
|
|
amulet.raise_status(amulet.FAIL, msg=msg)
|
|
|
|
def test_cinder_rootwrap_config(self):
|
|
''' Inspect select config pairs in rootwrap.conf. '''
|
|
unit_ci = self.cinder_sentry
|
|
conf = '/etc/cinder/rootwrap.conf'
|
|
expected = {'filters_path': '/etc/cinder/rootwrap.d,'
|
|
'/usr/share/cinder/rootwrap'}
|
|
section = 'DEFAULT'
|
|
|
|
if self._get_openstack_release() >= self.precise_havana:
|
|
expected['use_syslog'] = 'False'
|
|
expected['exec_dirs'] = '/sbin,/usr/sbin,/bin,/usr/bin'
|
|
|
|
ret = u.validate_config_data(unit_ci, conf, section, expected)
|
|
if ret:
|
|
msg = "cinder rootwrap config error: {}".format(ret)
|
|
amulet.raise_status(amulet.FAIL, msg=msg)
|
|
|
|
def test_cinder_endpoint(self):
|
|
'''Verify the cinder endpoint data.'''
|
|
endpoints = self.keystone.endpoints.list()
|
|
admin_port = internal_port = public_port = '8776'
|
|
expected = {'id': u.not_null,
|
|
'region': 'RegionOne',
|
|
'adminurl': u.valid_url,
|
|
'internalurl': u.valid_url,
|
|
'publicurl': u.valid_url,
|
|
'service_id': u.not_null}
|
|
|
|
ret = u.validate_endpoint_data(endpoints, admin_port, internal_port,
|
|
public_port, expected)
|
|
if ret:
|
|
amulet.raise_status(amulet.FAIL,
|
|
msg='glance endpoint: {}'.format(ret))
|
|
|
|
def test_z_cinder_restart_on_config_change(self):
|
|
'''Verify cinder services are restarted when the config is changed.
|
|
|
|
Note(coreycb): The method name with the _z_ is a little odd
|
|
but it forces the test to run last. It just makes things
|
|
easier because restarting services requires re-authorization.
|
|
'''
|
|
u.log.debug('making charm config change')
|
|
mtime = u.get_sentry_time(self.cinder_sentry)
|
|
self.d.configure('cinder', {'verbose': 'True', 'debug': 'True'})
|
|
if not u.validate_service_config_changed(self.cinder_sentry,
|
|
mtime,
|
|
'cinder-api',
|
|
'/etc/cinder/cinder.conf'):
|
|
self.d.configure('cinder', {'verbose': 'False', 'debug': 'False'})
|
|
msg = "cinder-api service didn't restart after config change"
|
|
amulet.raise_status(amulet.FAIL, msg=msg)
|
|
|
|
if not u.validate_service_config_changed(self.cinder_sentry,
|
|
mtime,
|
|
'cinder-volume',
|
|
'/etc/cinder/cinder.conf',
|
|
sleep_time=0):
|
|
self.d.configure('cinder', {'verbose': 'False', 'debug': 'False'})
|
|
msg = "cinder-volume service didn't restart after conf change"
|
|
amulet.raise_status(amulet.FAIL, msg=msg)
|
|
|
|
u.log.debug('returning to original charm config')
|
|
self.d.configure('cinder', {'verbose': 'False', 'debug': 'False'})
|
|
|
|
def test_users(self):
|
|
'''Verify expected users.'''
|
|
user0 = {'name': 'cinder',
|
|
'enabled': True,
|
|
'tenantId': u.not_null,
|
|
'id': u.not_null,
|
|
'email': 'juju@localhost'}
|
|
user1 = {'name': 'admin',
|
|
'enabled': True,
|
|
'tenantId': u.not_null,
|
|
'id': u.not_null,
|
|
'email': 'juju@localhost'}
|
|
user2 = {'name': 'glance',
|
|
'enabled': True,
|
|
'tenantId': u.not_null,
|
|
'id': u.not_null,
|
|
'email': 'juju@localhost'}
|
|
expected = [user0, user1, user2]
|
|
actual = self.keystone.users.list()
|
|
|
|
ret = u.validate_user_data(expected, actual)
|
|
if ret:
|
|
amulet.raise_status(amulet.FAIL, msg=ret)
|
|
|
|
def test_000_delete_volumes_snapshots_images(self):
|
|
'''Delete all volumes, snapshots and images, if they exist,
|
|
as the first of the ordered tests. Useful in re-run scenarios.'''
|
|
self.test_900_delete_all_snapshots()
|
|
self.test_900_glance_delete_all_images()
|
|
self.test_999_delete_all_volumes()
|
|
|
|
def test_100_create_and_extend_volume(self):
|
|
'''Add and confirm a new 1GB volume. In Havana and later,
|
|
extend that volume to 2GB.'''
|
|
# Create new volume
|
|
vol_new = self.cinder.volumes.create(display_name="demo-vol", size=1)
|
|
vol_id = vol_new.id
|
|
|
|
# Wait for volume status to be available
|
|
ret = self.obj_is_status(self.cinder.volumes, obj_id=vol_id,
|
|
stat='available',
|
|
msg='create vol status wait')
|
|
if not ret:
|
|
msg = 'volume create failed'
|
|
amulet.raise_status(amulet.FAIL, msg=msg)
|
|
|
|
# NOTE(beisner): Cinder extend is supported only in Havana or later
|
|
if self._get_openstack_release() < self.precise_havana:
|
|
u.log.debug('Skipping volume extend due to openstack release < H')
|
|
return
|
|
|
|
# Extend volume size
|
|
self.cinder.volumes.extend(vol_id, '2')
|
|
|
|
# Wait for extend
|
|
vol_size = self.cinder.volumes.get(vol_id).size
|
|
tries = 0
|
|
while vol_size != 2 and tries <= 15:
|
|
u.log.debug('volume extend size wait: {} {}'.format(tries,
|
|
vol_id))
|
|
sleep(4)
|
|
vol_size = self.cinder.volumes.get(vol_id).size
|
|
tries += 1
|
|
|
|
if vol_size != 2:
|
|
msg = 'Failed to extend volume, size is {}'.format(vol_size)
|
|
amulet.raise_status(amulet.FAIL, msg=msg)
|
|
|
|
def test_100_glance_image_create(self):
|
|
'''Create new cirros glance image, to be referenced by
|
|
a cinder volume create tests in Havana or later.'''
|
|
|
|
# NOTE(beisner): Cinder create vol-from-img support for lvm and
|
|
# rbd(ceph) exists only in Havana or later
|
|
if self._get_openstack_release() < self.precise_havana:
|
|
u.log.debug('Skipping create glance img due to openstack rel < H')
|
|
return
|
|
|
|
# Create a new image
|
|
image_new = u.create_cirros_image(self.glance, 'cirros-image-1')
|
|
|
|
# Confirm image is created and has status of 'active'
|
|
if not image_new:
|
|
msg = 'image create failed'
|
|
amulet.raise_status(amulet.FAIL, msg=msg)
|
|
|
|
def test_200_clone_volume(self):
|
|
'''Create a new cinder volume, clone it to another cinder volume.'''
|
|
# Get volume object and ID
|
|
try:
|
|
vol = self.cinder.volumes.find(display_name="demo-vol")
|
|
vol_id = vol.id
|
|
vol_size = vol.size
|
|
except:
|
|
msg = ('Volume (demo-vol) not found.')
|
|
amulet.raise_status(amulet.FAIL, msg=msg)
|
|
|
|
if vol.status != 'available':
|
|
msg = ('volume status not == available: {}'.format(vol.status))
|
|
amulet.raise_status(amulet.FAIL, msg=msg)
|
|
|
|
# Create new clone volume from source volume
|
|
vol_clone = self.cinder.volumes.create(display_name="demo-vol-clone",
|
|
size=vol_size,
|
|
source_volid=vol_id)
|
|
|
|
ret = self.obj_is_status(self.cinder.volumes, obj_id=vol_clone.id,
|
|
stat='available',
|
|
msg='clone vol status wait')
|
|
if not ret:
|
|
msg = 'volume clone failed - from {}'.format(vol_id)
|
|
amulet.raise_status(amulet.FAIL, msg=msg)
|
|
|
|
def test_200_create_volume_from_glance_image(self):
|
|
'''Create new volume from glance cirros image (Havana and later),
|
|
check status and bootable flag.'''
|
|
|
|
# NOTE(beisner): Cinder create vol-from-img support for lvm and
|
|
# rbd(ceph) exists only in Havana or later
|
|
if self._get_openstack_release() < self.precise_havana:
|
|
u.log.debug('Skipping create vol from img, openstack rel < H')
|
|
return
|
|
|
|
# Get image object and id
|
|
expected_img_name = 'cirros-image-1'
|
|
img_list = list(self.glance.images.list())
|
|
img_count = len(img_list)
|
|
|
|
if img_count != 0:
|
|
# NOTE(beisner): glance api has no find method, presume 1st image
|
|
img_id = img_list[0].id
|
|
else:
|
|
msg = 'image not found'
|
|
amulet.raise_status(amulet.FAIL, msg=msg)
|
|
|
|
# Confirm image name
|
|
if img_list[0].name != expected_img_name:
|
|
msg = 'unexpected image name {}'.format(img_list[0].name)
|
|
amulet.raise_status(amulet.FAIL, msg=msg)
|
|
|
|
# Create new volume from glance image
|
|
vol_new = self.cinder.volumes.create(display_name="demo-vol-cirros",
|
|
size=1, imageRef=img_id)
|
|
vol_id = vol_new.id
|
|
|
|
# Wait for volume stat to be avail, check that it's flagged bootable
|
|
ret = self.obj_is_status(self.cinder.volumes, obj_id=vol_id,
|
|
stat='available',
|
|
msg='create vol from img status wait')
|
|
vol_boot = self.cinder.volumes.get(vol_id).bootable
|
|
|
|
if not ret or vol_boot != 'true':
|
|
vol_stat = self.cinder.volumes.get(vol_id).status
|
|
msg = ('vol create failed - from glance img:'
|
|
' id:{} stat:{} boot:{}'.format(vol_id,
|
|
vol_stat,
|
|
vol_boot))
|
|
amulet.raise_status(amulet.FAIL, msg=msg)
|
|
|
|
def test_300_cinder_create_snapshot(self):
|
|
'''Create a snapshot of a volume. Use a cirros-based volume where
|
|
supported (Havana and newer), and fall back to a vanilla
|
|
volume snapshot everywhere else.'''
|
|
|
|
if self._get_openstack_release() >= self.precise_havana:
|
|
vol_src_name = "demo-vol-cirros"
|
|
elif self._get_openstack_release() < self.precise_havana:
|
|
vol_src_name = "demo-vol"
|
|
|
|
u.log.debug('creating snapshot of volume: {}'.format(vol_src_name))
|
|
|
|
# Get volume object and id
|
|
try:
|
|
vol_src = self.cinder.volumes.find(display_name=vol_src_name)
|
|
vol_id = vol_src.id
|
|
except:
|
|
msg = ('volume not found while creating snapshot')
|
|
amulet.raise_status(amulet.FAIL, msg=msg)
|
|
|
|
if vol_src.status != 'available':
|
|
msg = ('volume status not == available: {}').format(vol_src.status)
|
|
amulet.raise_status(amulet.FAIL, msg=msg)
|
|
|
|
# Create new snapshot
|
|
snap_new = self.cinder.volume_snapshots.create(
|
|
volume_id=vol_id, display_name='demo-snapshot')
|
|
snap_id = snap_new.id
|
|
|
|
# Wait for snapshot status to become available
|
|
ret = self.obj_is_status(self.cinder.volume_snapshots, obj_id=snap_id,
|
|
stat='available',
|
|
msg='snapshot create status wait')
|
|
if not ret:
|
|
snap_stat = self.cinder.volume_snapshots.get(snap_id).status
|
|
msg = 'volume snapshot failed: {} {}'.format(snap_id,
|
|
snap_stat)
|
|
amulet.raise_status(amulet.FAIL, msg=msg)
|
|
|
|
def test_310_create_volume_from_snapshot(self):
|
|
'''Create a new volume from a snapshot of a volume.'''
|
|
# Get snapshot object and ID
|
|
try:
|
|
snap = self.cinder.volume_snapshots.find(
|
|
display_name="demo-snapshot")
|
|
snap_id = snap.id
|
|
snap_size = snap.size
|
|
except:
|
|
msg = 'snapshot not found while creating volume'
|
|
amulet.raise_status(amulet.FAIL, msg=msg)
|
|
|
|
if snap.status != 'available':
|
|
msg = 'snapshot status not == available: {}'.format(snap.status)
|
|
amulet.raise_status(amulet.FAIL, msg=msg)
|
|
|
|
# Create new volume from snapshot
|
|
vol_new = self.cinder.volumes.create(
|
|
display_name="demo-vol-from-snap",
|
|
snapshot_id=snap_id,
|
|
size=snap_size)
|
|
vol_id = vol_new.id
|
|
|
|
# Wait for volume status to be == available
|
|
ret = self.obj_is_status(self.cinder.volumes, obj_id=vol_id,
|
|
stat='available',
|
|
msg='vol from snap create status wait')
|
|
if not ret:
|
|
vol_stat = self.cinder.volumes.get(vol_id).status
|
|
msg = 'volume create failed: {} {}'.format(vol_id,
|
|
vol_stat)
|
|
amulet.raise_status(amulet.FAIL, msg=msg)
|
|
|
|
def test_900_confirm_lvm_volume_list(self):
|
|
'''Confirm cinder volume IDs with lvm logical volume IDs.
|
|
Expect a 1:1 relationship of lvm:cinder volumes.'''
|
|
commando = self.cinder_sentry.run('sudo lvs | grep cinder-volumes | '
|
|
'awk \'{ print $1 }\'')
|
|
vol_list = self.cinder.volumes.list()
|
|
lv_id_list = commando[0].split('\n')
|
|
vol_count = len(vol_list)
|
|
snap_count = len(self.cinder.volume_snapshots.list())
|
|
|
|
# Expect cinder vol + snap count to match lvm log vol count
|
|
if (vol_count + snap_count) != len(lv_id_list):
|
|
msg = ('lvm volume count ({}) != cinder volume + snap count '
|
|
'({})'.format(len(vol_list), len(lv_id_list)))
|
|
amulet.raise_status(amulet.FAIL, msg=msg)
|
|
|
|
# Expect all cinder vol IDs to exist in the LVM volume list
|
|
for vol_this in vol_list:
|
|
try:
|
|
lv_id_list.index('volume-' + vol_this.id)
|
|
except:
|
|
msg = ('volume ID {} not found in '
|
|
'LVM volume list.'.format(vol_this.id))
|
|
amulet.raise_status(amulet.FAIL, msg=msg)
|
|
|
|
def test_900_glance_delete_all_images(self):
|
|
'''Delete all glance images and confirm deletion.'''
|
|
ret = self.delete_all_objs(self.glance.images, item_desc='image')
|
|
if ret:
|
|
amulet.raise_status(amulet.FAIL, msg=ret)
|
|
|
|
def test_900_delete_all_snapshots(self):
|
|
'''Delete all cinder volume snapshots and confirm deletion.'''
|
|
ret = self.delete_all_objs(self.cinder.volume_snapshots,
|
|
item_desc='snapshot')
|
|
if ret:
|
|
amulet.raise_status(amulet.FAIL, msg=ret)
|
|
|
|
def test_999_delete_all_volumes(self):
|
|
'''Delete all cinder volumes and confirm deletion,
|
|
as the last of the ordered tests.'''
|
|
ret = self.delete_all_objs(self.cinder.volumes, item_desc='volume')
|
|
if ret:
|
|
amulet.raise_status(amulet.FAIL, msg=ret)
|