493 lines
18 KiB
Python
493 lines
18 KiB
Python
import os
|
|
import re
|
|
import subprocess
|
|
import time
|
|
import urllib
|
|
|
|
import glanceclient
|
|
import keystoneauth1
|
|
import keystoneauth1.identity.v2 as keystoneauth1_v2
|
|
import keystoneauth1.session as keystoneauth1_session
|
|
import keystoneclient.v2_0.client as keystoneclient_v2
|
|
import keystoneclient.v3.client as keystoneclient_v3
|
|
import keystoneclient.auth.identity.v3 as keystone_id_v3
|
|
import keystoneclient.session as session
|
|
import neutronclient.v2_0.client as neutronclient
|
|
import novaclient.client as novaclient_client
|
|
|
|
import charms_openstack.charm as charm
|
|
import charms_openstack.adapters as adapters
|
|
import charmhelpers.core.hookenv as hookenv
|
|
import charmhelpers.core.host as host
|
|
import charmhelpers.fetch as fetch
|
|
|
|
|
|
def install():
|
|
"""Use the singleton from the TempestCharm to install the packages on the
|
|
unit
|
|
"""
|
|
TempestCharm.singleton.install()
|
|
|
|
|
|
def render_configs(interfaces_list):
|
|
"""Using a list of interfaces, render the configs and, if they have
|
|
changes, restart the services on the unit.
|
|
"""
|
|
if not os.path.isdir(TempestCharm.TEMPEST_LOGDIR):
|
|
os.makedirs(TempestCharm.TEMPEST_LOGDIR)
|
|
TempestCharm.singleton.render_with_interfaces(interfaces_list)
|
|
TempestCharm.singleton.assess_status()
|
|
|
|
|
|
def run_test(tox_target):
|
|
"""Use the singleton from the TempestCharm to install the packages on the
|
|
unit
|
|
"""
|
|
TempestCharm.singleton.run_test(tox_target)
|
|
|
|
|
|
def assess_status():
|
|
"""Use the singleton from the TempestCharm to install the packages on the
|
|
unit
|
|
"""
|
|
TempestCharm.singleton.assess_status()
|
|
|
|
|
|
class TempestAdminAdapter(adapters.OpenStackRelationAdapter):
|
|
|
|
"""Inspect relations and provide properties that can be used when
|
|
rendering templates"""
|
|
|
|
interface_type = "identity-admin"
|
|
|
|
def __init__(self, relation):
|
|
"""Initialise a keystone client and collect user defined config"""
|
|
self.kc = None
|
|
self.keystone_session = None
|
|
self.api_version = '2'
|
|
super(TempestAdminAdapter, self).__init__(relation)
|
|
self.init_keystone_client()
|
|
self.uconfig = hookenv.config()
|
|
|
|
@property
|
|
def keystone_info(self):
|
|
"""Collection keystone information from keystone relation"""
|
|
ks_info = self.relation.credentials()
|
|
ks_info['default_credentials_domain_name'] = 'default'
|
|
if ks_info.get('api_version'):
|
|
ks_info['api_version'] = ks_info.get('api_version')
|
|
else:
|
|
ks_info['api_version'] = self.api_version
|
|
if not ks_info.get('service_user_domain_name'):
|
|
ks_info['service_user_domain_name'] = 'admin_domain'
|
|
|
|
return ks_info
|
|
|
|
@property
|
|
def ks_client(self):
|
|
if not self.kc:
|
|
self.init_keystone_client()
|
|
return self.kc
|
|
|
|
def keystone_auth_url(self, api_version=None):
|
|
if not api_version:
|
|
api_version = self.keystone_info.get('api_version', '2')
|
|
ep_suffix = {
|
|
'2': 'v2.0',
|
|
'3': 'v3'}[api_version]
|
|
return '{}://{}:{}/{}'.format(
|
|
'http',
|
|
self.keystone_info['service_hostname'],
|
|
self.keystone_info['service_port'],
|
|
ep_suffix,
|
|
)
|
|
|
|
def resolve_endpoint(self, service_type, interface):
|
|
if self.api_version == '2':
|
|
ep = self.ks_client.service_catalog.url_for(
|
|
service_type=service_type,
|
|
endpoint_type='{}URL'.format(interface)
|
|
)
|
|
else:
|
|
svc_id = self.ks_client.services.find(type=service_type).id
|
|
ep = self.ks_client.endpoints.find(
|
|
service_id=svc_id,
|
|
interface=interface).url
|
|
return ep
|
|
|
|
def set_keystone_v2_client(self):
|
|
self.keystone_session = None
|
|
self.kc = keystoneclient_v2.Client(**self.admin_creds_v2)
|
|
|
|
def set_keystone_v3_client(self):
|
|
auth = keystone_id_v3.Password(**self.admin_creds_v3)
|
|
self.keystone_session = session.Session(auth=auth)
|
|
self.kc = keystoneclient_v3.Client(session=self.keystone_session)
|
|
|
|
def init_keystone_client(self):
|
|
"""Initialise keystone client"""
|
|
if self.kc:
|
|
return
|
|
if self.keystone_info.get('api_version', '2') > '2':
|
|
self.set_keystone_v3_client()
|
|
self.api_version = '3'
|
|
else:
|
|
# XXX Temporarily catching the Unauthorized exception to deal with
|
|
# the case (pre-17.02) where the keystone charm maybe in v3 mode
|
|
# without telling charms via the identity-admin relation
|
|
try:
|
|
self.set_keystone_v2_client()
|
|
self.api_version = '2'
|
|
except keystoneauth1.exceptions.http.Unauthorized:
|
|
self.set_keystone_v3_client()
|
|
self.api_version = '3'
|
|
self.kc.services.list()
|
|
|
|
def admin_creds_base(self, api_version):
|
|
return {
|
|
'username': self.keystone_info['service_username'],
|
|
'password': self.keystone_info['service_password'],
|
|
'auth_url': self.keystone_auth_url(api_version=api_version)}
|
|
|
|
@property
|
|
def admin_creds_v2(self):
|
|
creds = self.admin_creds_base(api_version='2')
|
|
creds['tenant_name'] = self.keystone_info['service_tenant_name']
|
|
creds['region_name'] = self.keystone_info['service_region']
|
|
return creds
|
|
|
|
@property
|
|
def admin_creds_v3(self):
|
|
creds = self.admin_creds_base(api_version='3')
|
|
creds['project_name'] = self.keystone_info.get(
|
|
'service_project_name',
|
|
'admin')
|
|
creds['user_domain_name'] = self.keystone_info.get(
|
|
'service_user_domain_name',
|
|
'admin_domain')
|
|
creds['project_domain_name'] = self.keystone_info.get(
|
|
'service_project_domain_name',
|
|
'Default')
|
|
return creds
|
|
|
|
@property
|
|
def ec2_creds(self):
|
|
"""Generate EC2 style tokens or return existing EC2 tokens
|
|
|
|
@returns {'access_token' token1, 'secret_token': token2}
|
|
"""
|
|
_ec2creds = {}
|
|
if self.api_version == '2':
|
|
current_creds = self.ks_client.ec2.list(self.ks_client.user_id)
|
|
if current_creds:
|
|
_ec2creds = current_creds[0]
|
|
else:
|
|
creds = self.ks_client.ec2.create(
|
|
self.ks_client.user_id,
|
|
self.ks_client.tenant_id)
|
|
_ec2creds = {
|
|
'access_token': creds.access,
|
|
'secret_token': creds.secret}
|
|
return _ec2creds
|
|
|
|
@property
|
|
def image_info(self):
|
|
"""Return image ids for the user-defined image names
|
|
|
|
@returns {'image_id' id1, 'image_alt_id': id2}
|
|
"""
|
|
image_info = {}
|
|
if self.service_present('glance'):
|
|
if self.keystone_session:
|
|
glance_client = glanceclient.Client(
|
|
'2', session=self.keystone_session)
|
|
else:
|
|
glance_ep = self.resolve_endpoint('image', 'public')
|
|
glance_client = glanceclient.Client(
|
|
'2', glance_ep, token=self.ks_client.auth_token)
|
|
for image in glance_client.images.list():
|
|
if self.uconfig.get('glance-image-name') == image.name:
|
|
image_info['image_id'] = image.id
|
|
if self.uconfig.get('image-ssh-user'):
|
|
image_info['image_ssh_user'] = \
|
|
self.uconfig.get('image-ssh-user')
|
|
if self.uconfig.get('glance-alt-image-name') == image.name:
|
|
image_info['image_alt_id'] = image.id
|
|
if self.uconfig.get('image-alt-ssh-user'):
|
|
image_info['image_alt_ssh_user'] = \
|
|
self.uconfig.get('image-alt-ssh-user')
|
|
return image_info
|
|
|
|
@property
|
|
def network_info(self):
|
|
"""Return public network and router ids for user-defined router and
|
|
network names
|
|
|
|
@returns {'public_network_id' id1, 'router_id': id2}
|
|
"""
|
|
network_info = {}
|
|
if self.service_present('neutron'):
|
|
if self.keystone_session:
|
|
neutron_client = neutronclient.Client(
|
|
session=self.keystone_session)
|
|
else:
|
|
neutron_ep = self.ks_client.service_catalog.url_for(
|
|
service_type='network',
|
|
endpoint_type='publicURL')
|
|
neutron_client = neutronclient.Client(
|
|
endpoint_url=neutron_ep,
|
|
token=self.ks_client.auth_token)
|
|
routers = neutron_client.list_routers(
|
|
name=self.uconfig['router-name'])
|
|
if len(routers['routers']) == 0:
|
|
hookenv.log("Router not found")
|
|
else:
|
|
router = routers['routers'][0]
|
|
network_info['router_id'] = router['id']
|
|
networks = neutron_client.list_networks(
|
|
name=self.uconfig['network-name'])
|
|
if len(networks['networks']) == 0:
|
|
hookenv.log("network not found")
|
|
else:
|
|
network = networks['networks'][0]
|
|
network_info['public_network_id'] = network['id']
|
|
networks = neutron_client.list_networks(
|
|
name=self.uconfig['floating-network-name'])
|
|
if len(networks['networks']) == 0:
|
|
hookenv.log("Floating network name not found")
|
|
else:
|
|
network_info['floating_network_name'] = \
|
|
self.uconfig['floating-network-name']
|
|
return network_info
|
|
|
|
def service_present(self, service):
|
|
"""Check if a given service type is registered in the catalogue
|
|
|
|
:params service: string Service type
|
|
@returns Boolean: True if service is registered
|
|
"""
|
|
return service in self.get_present_services()
|
|
|
|
def get_nova_client(self):
|
|
if not self.keystone_session:
|
|
auth = keystoneauth1_v2.Password(
|
|
auth_url=self.keystone_auth_url(),
|
|
username=self.keystone_info['service_username'],
|
|
password=self.keystone_info['service_password'],
|
|
tenant_name=self.keystone_info['service_tenant_name'])
|
|
self.keystone_session = keystoneauth1_session.Session(auth=auth)
|
|
return novaclient_client.Client(
|
|
2, session=self.keystone_session)
|
|
|
|
@property
|
|
def compute_info(self):
|
|
"""Return flavor ids for user-defined flavors
|
|
|
|
@returns {'flavor_id' id1, 'flavor_alt_id': id2}
|
|
"""
|
|
compute_info = {}
|
|
if self.service_present('nova'):
|
|
nova_client = self.get_nova_client()
|
|
nova_ep = self.resolve_endpoint('compute', 'public')
|
|
url = urllib.parse.urlparse(nova_ep)
|
|
compute_info['nova_base'] = '{}://{}'.format(
|
|
url.scheme,
|
|
url.netloc.split(':')[0])
|
|
for flavor in nova_client.flavors.list():
|
|
if self.uconfig['flavor-name'] == flavor.name:
|
|
compute_info['flavor_id'] = flavor.id
|
|
if self.uconfig['flavor-alt-name'] == flavor.name:
|
|
compute_info['flavor_alt_id'] = flavor.id
|
|
return compute_info
|
|
|
|
def get_present_services(self):
|
|
"""Query keystone catalogue for a list for registered services
|
|
|
|
@returns [svc1, svc2, ...]: List of registered services
|
|
"""
|
|
services = [svc.name
|
|
for svc in self.ks_client.services.list()
|
|
if svc.enabled]
|
|
return services
|
|
|
|
@property
|
|
def service_info(self):
|
|
"""Assemble a list of services tempest should tests
|
|
|
|
Compare the list of keystone registered services with the services the
|
|
user has requested be tested. If in 'auto' mode test all services
|
|
registered in keystone.
|
|
|
|
@returns [svc1, svc2, ...]: List of services to test
|
|
"""
|
|
service_info = {}
|
|
tempest_candidates = ['ceilometer', 'cinder', 'glance', 'heat',
|
|
'horizon', 'ironic', 'neutron', 'nova',
|
|
'sahara', 'swift', 'trove', 'zaqar', 'neutron']
|
|
present_svcs = self.get_present_services()
|
|
# If not running in an action context asssume auto mode
|
|
try:
|
|
action_args = hookenv.action_get()
|
|
except Exception as e:
|
|
action_args = {'service-whitelist': 'auto'}
|
|
if action_args['service-whitelist'] == 'auto':
|
|
white_list = []
|
|
for svc in present_svcs:
|
|
if svc in tempest_candidates:
|
|
white_list.append(svc)
|
|
else:
|
|
white_list = action_args['service-whitelist']
|
|
for svc in tempest_candidates:
|
|
if svc in white_list:
|
|
service_info[svc] = 'true'
|
|
else:
|
|
service_info[svc] = 'false'
|
|
return service_info
|
|
|
|
|
|
class TempestAdapters(adapters.OpenStackRelationAdapters):
|
|
"""
|
|
Adapters class for the Tempest charm.
|
|
"""
|
|
relation_adapters = {
|
|
'identity_admin': TempestAdminAdapter,
|
|
}
|
|
|
|
def __init__(self, relations):
|
|
super(TempestAdapters, self).__init__(
|
|
relations,
|
|
options=TempestConfigurationAdapter)
|
|
|
|
|
|
class TempestConfigurationAdapter(adapters.ConfigurationAdapter):
|
|
"""
|
|
Manipulate user supplied config as needed
|
|
"""
|
|
def __init__(self):
|
|
super(TempestConfigurationAdapter, self).__init__()
|
|
|
|
|
|
class TempestCharm(charm.OpenStackCharm):
|
|
|
|
release = 'liberty'
|
|
name = 'tempest'
|
|
|
|
required_relations = ['identity-admin']
|
|
"""Directories and files used for running tempest"""
|
|
TEMPEST_ROOT = '/var/lib/tempest'
|
|
TEMPEST_LOGDIR = TEMPEST_ROOT + '/logs'
|
|
TEMPEST_CONF = TEMPEST_ROOT + '/tempest.conf'
|
|
"""pip.conf for proxy settings etc"""
|
|
PIP_CONF = '/root/.pip/pip.conf'
|
|
|
|
"""List of packages charm should install
|
|
XXX The install hook is currently installing most packages ahead of
|
|
this because modules like keystoneclient are needed at load time
|
|
"""
|
|
packages = [
|
|
'git', 'testrepository', 'subunit', 'python-nose', 'python-lxml',
|
|
'python-boto', 'python-junitxml', 'python-subunit',
|
|
'python-testresources', 'python-oslotest', 'python-stevedore',
|
|
'python-cinderclient', 'python-glanceclient', 'python-heatclient',
|
|
'python-keystoneclient', 'python-neutronclient', 'python-novaclient',
|
|
'python-swiftclient', 'python-ceilometerclient', 'openvswitch-test',
|
|
'python3-cinderclient', 'python3-glanceclient', 'python3-heatclient',
|
|
'python3-keystoneclient', 'python3-neutronclient',
|
|
'python3-novaclient', 'python3-swiftclient',
|
|
'python3-ceilometerclient', 'openvswitch-common', 'libffi-dev',
|
|
'libssl-dev', 'python-dev', 'python-cffi'
|
|
]
|
|
|
|
"""Use the Tempest specific adapters"""
|
|
adapters_class = TempestAdapters
|
|
"""Tempest has no running services so no services need restarting on
|
|
config file change
|
|
"""
|
|
restart_map = {
|
|
TEMPEST_CONF: [],
|
|
PIP_CONF: [],
|
|
}
|
|
|
|
@property
|
|
def all_packages(self):
|
|
_packages = self.packages[:]
|
|
if host.lsb_release()['DISTRIB_RELEASE'] > '14.04':
|
|
_packages.append('tox')
|
|
else:
|
|
_packages.append('python-tox')
|
|
return _packages
|
|
|
|
def setup_directories(self):
|
|
for tempest_dir in [self.TEMPEST_ROOT, self.TEMPEST_LOGDIR]:
|
|
if not os.path.exists(tempest_dir):
|
|
os.mkdir(tempest_dir)
|
|
|
|
def setup_git(self, branch, git_dir):
|
|
"""Clone tempest and symlink in rendered tempest.conf"""
|
|
conf = hookenv.config()
|
|
if not os.path.exists(git_dir):
|
|
git_url = conf['tempest-source']
|
|
fetch.install_remote(str(git_url), dest=str(git_dir),
|
|
branch=str(branch), depth=str(1))
|
|
conf_symlink = git_dir + '/tempest/etc/tempest.conf'
|
|
if not os.path.exists(conf_symlink):
|
|
os.symlink(self.TEMPEST_CONF, conf_symlink)
|
|
|
|
def execute_tox(self, run_dir, logfile, tox_target):
|
|
"""Trigger tempest run through tox setting proxies if needed"""
|
|
env = os.environ.copy()
|
|
conf = hookenv.config()
|
|
if conf.get('http-proxy'):
|
|
env['http_proxy'] = conf['http-proxy']
|
|
if conf.get('https-proxy'):
|
|
env['https_proxy'] = conf['https-proxy']
|
|
cmd = ['tox', '-e', tox_target]
|
|
f = open(logfile, "w")
|
|
subprocess.call(cmd, cwd=run_dir, stdout=f, stderr=f, env=env)
|
|
|
|
def get_tempest_files(self, branch_name):
|
|
"""Prepare tempest files and directories
|
|
|
|
@return git_dir, logfile, run_dir
|
|
"""
|
|
log_time_str = time.strftime("%Y%m%d%H%M%S", time.gmtime())
|
|
git_dir = '{}/tempest-{}'.format(self.TEMPEST_ROOT, branch_name)
|
|
logfile = '{}/run_{}.log'.format(self.TEMPEST_LOGDIR, log_time_str)
|
|
run_dir = '{}/tempest'.format(git_dir)
|
|
return git_dir, logfile, run_dir
|
|
|
|
def parse_tempest_log(self, logfile):
|
|
"""Read tempest logfile and return summary as dict
|
|
|
|
@return dict: Dictonary of summary data
|
|
"""
|
|
summary = {}
|
|
with open(logfile, 'r') as tempest_log:
|
|
summary_line = False
|
|
for line in tempest_log:
|
|
if line.strip() == "Totals":
|
|
summary_line = True
|
|
if line.strip() == "Worker Balance":
|
|
summary_line = False
|
|
if summary_line:
|
|
# Match lines like: ' - Unexpected Success: 0'
|
|
matchObj = re.match(
|
|
r'(.*)- (.*?):\s+(.*)', line, re.M | re.I)
|
|
if matchObj:
|
|
key = matchObj.group(2)
|
|
key = key.replace(' ', '-').replace(':', '').lower()
|
|
summary[key] = matchObj.group(3)
|
|
return summary
|
|
|
|
def run_test(self, tox_target):
|
|
"""Run smoke tests"""
|
|
action_args = hookenv.action_get()
|
|
branch_name = action_args['branch']
|
|
git_dir, logfile, run_dir = self.get_tempest_files(branch_name)
|
|
self.setup_directories()
|
|
self.setup_git(branch_name, git_dir)
|
|
self.execute_tox(run_dir, logfile, tox_target)
|
|
action_info = self.parse_tempest_log(logfile)
|
|
action_info['tempest-logfile'] = logfile
|
|
hookenv.action_set(action_info)
|