import re import os import subprocess import time import glanceclient import keystoneclient.v2_0 as keystoneclient import neutronclient.v2_0.client as neutronclient import novaclient.v2 as novaclient import urllib import charms_openstack.charm as charm import charms_openstack.adapters as adapters import charmhelpers.core.hookenv as hookenv 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 super(TempestAdminAdapter, self).__init__(relation) self.init_keystone_client() self.uconfig = hookenv.config() @property def keystone_info(self): """Collection keystone information from keystone relation""" return self.relation.credentials() @property def ks_client(self): if not self.kc: self.init_keystone_client() return self.kc @property def keystone_auth_url(self): return '{}://{}:{}/v2.0'.format( 'http', self.keystone_info['service_hostname'], self.keystone_info['service_port'] ) def init_keystone_client(self): """Initialise keystone client""" if self.kc: return auth = { 'username': self.keystone_info['service_username'], 'password': self.keystone_info['service_password'], 'auth_url': self.keystone_auth_url, 'tenant_name': self.keystone_info['service_tenant_name'], 'region_name': self.keystone_info['service_region'], } try: self.kc = keystoneclient.client.Client(**auth) except: hookenv.log("Keystone is not ready, deferring keystone query") @property def ec2_creds(self): """Generate EC2 style tokens or return existing EC2 tokens @returns {'access_token' token1, 'secret_token': token2} """ if not self.ks_client: return {} current_creds = self.ks_client.ec2.list(self.ks_client.user_id) if current_creds: creds = current_creds[0] else: creds = self.ks_client.ec2.create( self.ks_client.user_id, self.ks_client.tenant_id) return {'access_token': creds.access, 'secret_token': creds.secret} @property def image_info(self): """Return image ids for the user-defined image names @returns {'image_id' id1, 'image_alt_id': id2} """ image_info = {} try: glance_endpoint = self.ks_client.service_catalog.url_for( service_type='image', endpoint_type='publicURL') glance_client = glanceclient.Client( '2', glance_endpoint, 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') except: hookenv.log("Glance is not ready, deferring glance query") 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 = {} try: 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'] except: hookenv.log("Neutron is not ready, deferring neutron query") return network_info @property def compute_info(self): """Return flavor ids for user-defined flavors @returns {'flavor_id' id1, 'flavor_alt_id': id2} """ compute_info = {} try: nova_ep = self.ks_client.service_catalog.url_for( service_type='compute', endpoint_type='publicURL' ) compute_info['nova_endpoint'] = nova_ep url = urllib.parse.urlparse(nova_ep) compute_info['nova_base'] = '{}://{}'.format( url.scheme, url.netloc.split(':')[0]) nova_client = novaclient.client.Client( self.keystone_info['service_username'], self.keystone_info['service_password'], self.keystone_info['service_tenant_name'], self.keystone_auth_url, ) 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 except: hookenv.log("Nova is not ready, deferring nova query") 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: 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: [], } 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)