diff --git a/config_tempest/api_discovery.py b/config_tempest/api_discovery.py deleted file mode 100644 index cba23509..00000000 --- a/config_tempest/api_discovery.py +++ /dev/null @@ -1,279 +0,0 @@ -#!/usr/bin/env python - -# Copyright 2013 Red Hat, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import json -import logging -import re -import requests -import urllib3 -import urlparse - -LOG = logging.getLogger(__name__) -MULTIPLE_SLASH = re.compile(r'/+') - - -class ServiceError(Exception): - pass - - -class Service(object): - def __init__(self, name, service_url, token, disable_ssl_validation): - self.name = name - self.service_url = service_url - self.headers = {'Accept': 'application/json', 'X-Auth-Token': token} - self.disable_ssl_validation = disable_ssl_validation - - def do_get(self, url, top_level=False, top_level_path=""): - parts = list(urlparse.urlparse(url)) - # 2 is the path offset - if top_level: - parts[2] = '/' + top_level_path - - parts[2] = MULTIPLE_SLASH.sub('/', parts[2]) - url = urlparse.urlunparse(parts) - - try: - if self.disable_ssl_validation: - urllib3.disable_warnings() - http = urllib3.PoolManager(cert_reqs='CERT_NONE') - else: - http = urllib3.PoolManager() - r = http.request('GET', url, headers=self.headers) - except Exception as e: - LOG.error("Request on service '%s' with url '%s' failed", - (self.name, url)) - raise e - if r.status >= 400: - raise ServiceError("Request on service '%s' with url '%s' failed" - " with code %d" % (self.name, url, r.status)) - return r.data - - def get_extensions(self): - return [] - - def get_versions(self): - return [] - - -class VersionedService(Service): - def get_versions(self, top_level=True): - body = self.do_get(self.service_url, top_level=top_level) - body = json.loads(body) - return self.deserialize_versions(body) - - def deserialize_versions(self, body): - return map(lambda x: x['id'], body['versions']) - - def no_port_cut_url(self): - # if there is no port defined, cut the url from version to the end - u = urllib3.util.parse_url(self.service_url) - url = self.service_url - if u.port is None: - found = re.findall(r'v\d', url) - if len(found) > 0: - index = url.index(found[0]) - url = self.service_url[:index] - return (url, u.port is not None) - - -class ComputeService(VersionedService): - def get_extensions(self): - body = self.do_get(self.service_url + '/extensions') - body = json.loads(body) - return map(lambda x: x['alias'], body['extensions']) - - def get_versions(self): - url, top_level = self.no_port_cut_url() - body = self.do_get(url, top_level=top_level) - body = json.loads(body) - return self.deserialize_versions(body) - - -class ImageService(VersionedService): - def get_versions(self): - return super(ImageService, self).get_versions(top_level=False) - - -class NetworkService(VersionedService): - def get_extensions(self): - body = self.do_get(self.service_url + '/v2.0/extensions.json') - body = json.loads(body) - return map(lambda x: x['alias'], body['extensions']) - - -class VolumeService(VersionedService): - def get_extensions(self): - body = self.do_get(self.service_url + '/extensions') - body = json.loads(body) - return map(lambda x: x['alias'], body['extensions']) - - def get_versions(self): - url, top_level = self.no_port_cut_url() - body = self.do_get(url, top_level=top_level) - body = json.loads(body) - return self.deserialize_versions(body) - - -class IdentityService(VersionedService): - def __init__(self, name, service_url, token, disable_ssl_validation): - super(VersionedService, self).__init__( - name, service_url, token, disable_ssl_validation) - version = '' - if 'v2' in self.service_url: - version = '/v2.0' - url_parse = urlparse.urlparse(self.service_url) - self.service_url = '{}://{}{}'.format(url_parse.scheme, - url_parse.netloc, version) - - def get_extensions(self): - if 'v2' in self.service_url: - body = self.do_get(self.service_url + '/extensions') - body = json.loads(body) - return map(lambda x: x['alias'], body['extensions']['values']) - # Keystone api changed in v3, the concept of extensions change. Right - # now, all the existin extensions are part of keystone core api, so, - # there's no longer the /extensions endpoint. The extensions that are - # stable, are enabled by default, the ones marked as experimental are - # disabled by default. Checking the tempest source, there's no test - # pointing to extensions endpoint, so I am very confident that this - # will not be an issue. If so, we need to list all the /OS-XYZ - # extensions to identify what is enabled or not. This would be a manual - # check every time keystone change, add or delete an extension, so I - # rather prefer to return empty here for now. - return [] - - def deserialize_versions(self, body): - try: - versions = [] - for v in body['versions']['values']: - # TripleO is in transition to v3 only, so the environment - # still returns v2 versions even though they're deprecated. - # Therefor pick only versions with stable status. - if v['status'] == 'stable': - versions.append(v['id']) - return versions - except KeyError: - return [body['version']['id']] - - def get_versions(self): - return super(IdentityService, self).get_versions(top_level=False) - - -class ObjectStorageService(Service): - def get_extensions(self): - body = self.do_get(self.service_url, top_level=True, - top_level_path="info") - body = json.loads(body) - # Remove Swift general information from extensions list - body.pop('swift') - return body.keys() - - -service_dict = {'compute': ComputeService, - 'image': ImageService, - 'network': NetworkService, - 'object-store': ObjectStorageService, - 'volumev3': VolumeService, - 'identity': IdentityService} - - -def get_service_class(service_name): - return service_dict.get(service_name, Service) - - -def get_identity_v3_extensions(keystone_v3_url): - """Returns discovered identity v3 extensions - - As keystone V3 uses a JSON Home to store the extensions, - this method is kept here just for the sake of functionality, but it - implements a different discovery method. - - :param keystone_v3_url: Keystone V3 auth url - :return: A list with the discovered extensions - """ - try: - r = requests.get(keystone_v3_url, - verify=False, - headers={'Accept': 'application/json-home'}) - except requests.exceptions.RequestException as re: - LOG.error("Request on service '%s' with url '%s' failed", - 'identity', keystone_v3_url) - raise re - ext_h = 'http://docs.openstack.org/api/openstack-identity/3/ext/' - res = [x for x in json.loads(r.content)['resources'].keys()] - ext = [ex for ex in res if 'ext' in ex] - return list(set([str(e).replace(ext_h, '').split('/')[0] for e in ext])) - - -def discover(auth_provider, region, object_store_discovery=True, - api_version=2, disable_ssl_certificate_validation=True): - """Returns a dict with discovered apis. - - :param auth_provider: An AuthProvider to obtain service urls. - :param region: A specific region to use. If the catalog has only one region - then that region will be used. - :return: A dict with an entry for the type of each discovered service. - Each entry has keys for 'extensions' and 'versions'. - """ - token, auth_data = auth_provider.get_auth() - services = {} - service_catalog = 'serviceCatalog' - public_url = 'publicURL' - identity_port = urlparse.urlparse(auth_provider.auth_url).port - if identity_port is None: - identity_port = "" - else: - identity_port = ":" + str(identity_port) - identity_version = urlparse.urlparse(auth_provider.auth_url).path - if api_version == 3: - service_catalog = 'catalog' - public_url = 'url' - - # FIXME(chandankumar): It is a workaround to filter services whose - # endpoints does not exist. Once it is merged. Let's rewrite the whole - # stuff. - auth_data[service_catalog] = [data for data in auth_data[service_catalog] - if data['endpoints']] - - for entry in auth_data[service_catalog]: - name = entry['type'] - services[name] = dict() - for _ep in entry['endpoints']: - if api_version == 3: - if _ep['region'] == region and _ep['interface'] == 'public': - ep = _ep - break - else: - if _ep['region'] == region: - ep = _ep - break - else: - ep = entry['endpoints'][0] - if 'identity' in ep[public_url]: - services[name]['url'] = ep[public_url].replace( - "/identity", "{0}{1}".format( - identity_port, identity_version)) - else: - services[name]['url'] = ep[public_url] - service_class = get_service_class(name) - service = service_class(name, services[name]['url'], token, - disable_ssl_certificate_validation) - if name == 'object-store' and not object_store_discovery: - services[name]['extensions'] = [] - elif 'v3' not in ep[public_url]: # is not v3 url - services[name]['extensions'] = service.get_extensions() - services[name]['versions'] = service.get_versions() - return services diff --git a/config_tempest/clients.py b/config_tempest/clients.py index 5fd5d599..23ab4227 100644 --- a/config_tempest/clients.py +++ b/config_tempest/clients.py @@ -1,17 +1,17 @@ -# Copyright 2018 Red Hat, Inc. +# Copyright 2016, 2018 Red Hat, Inc. # All Rights Reserved. # -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# http://www.apache.org/licenses/LICENSE-2.0 # -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. from tempest.lib import exceptions from tempest.lib.services.compute import flavors_client @@ -41,8 +41,10 @@ class ProjectsClient(object): def __init__(self, auth, catalog_type, identity_region, endpoint_type, identity_version, **default_params): self.identity_version = identity_version - self.project_class = tenants_client.TenantsClient if \ - self.identity_version == "v2" else projects_client.ProjectsClient + if self.identity_version == "v2": + self.project_class = tenants_client.TenantsClient + else: + self.project_class = projects_client.ProjectsClient self.client = self.project_class(auth, catalog_type, identity_region, endpoint_type, **default_params) @@ -76,24 +78,19 @@ class ClientManager(object): :param conf: TempestConf object :param creds: Credentials object """ - - self.identity_region = conf.get_defaulted('identity', 'region') + self.identity_region = creds.identity_region self.auth_provider = creds.get_auth_provider() - default_params = { - 'disable_ssl_certificate_validation': - conf.get_defaulted('identity', - 'disable_ssl_certificate_validation'), - 'ca_certs': conf.get_defaulted('identity', 'ca_certificates_file') - } - compute_params = { - 'service': conf.get_defaulted('compute', 'catalog_type'), - 'region': self.identity_region, - 'endpoint_type': conf.get_defaulted('compute', 'endpoint_type') - } + default_params = self._get_default_params(conf) + compute_params = self._get_compute_params(conf) compute_params.update(default_params) - self.identity = self.get_identity_client(conf, default_params) + catalog_type = conf.get_defaulted('identity', 'catalog_type') + + self.identity = self.get_identity_client( + creds.identity_version, + catalog_type, + default_params) self.tenants = ProjectsClient( self.auth_provider, @@ -105,15 +102,15 @@ class ClientManager(object): self.set_roles_client( auth=self.auth_provider, - creds=creds, - conf=conf, + identity_version=creds.identity_version, + catalog_type=catalog_type, endpoint_type='publicURL', default_params=default_params) self.set_users_client( auth=self.auth_provider, - creds=creds, - conf=conf, + identity_version=creds.identity_version, + catalog_type=catalog_type, endpoint_type='publicURL', default_params=default_params) @@ -134,7 +131,7 @@ class ClientManager(object): self.identity_region, **default_params) - self.volume_service = services_client.ServicesClient( + self.volume_client = services_client.ServicesClient( self.auth_provider, conf.get_defaulted('volume', 'catalog_type'), self.identity_region, @@ -167,65 +164,99 @@ class ClientManager(object): tenant = self.tenants.get_project_by_name(creds.tenant_name) conf.set('identity', 'admin_tenant_id', tenant['id']) - def get_identity_client(self, conf, default_params): + def _get_default_params(self, conf): + default_params = { + 'disable_ssl_certificate_validation': + conf.get_defaulted('identity', + 'disable_ssl_certificate_validation'), + 'ca_certs': conf.get_defaulted('identity', 'ca_certificates_file') + } + return default_params + + def _get_compute_params(self, conf): + compute_params = { + 'service': conf.get_defaulted('compute', 'catalog_type'), + 'region': self.identity_region, + 'endpoint_type': conf.get_defaulted('compute', 'endpoint_type') + } + return compute_params + + def get_identity_client(self, identity_version, catalog_type, + default_params): """Obtain identity client. - :type conf: TempestConf object + :type identity_version: string :type default_params: dict """ - if "v2.0" in conf.get("identity", "uri"): - return identity_client.IdentityClient( + if "v3" in identity_version: + return identity_v3_client.IdentityClient( self.auth_provider, - conf.get_defaulted('identity', 'catalog_type'), + catalog_type, self.identity_region, endpoint_type='publicURL', **default_params) else: - return identity_v3_client.IdentityClient( + return identity_client.IdentityClient( self.auth_provider, - conf.get_defaulted('identity', 'catalog_type'), + catalog_type, self.identity_region, endpoint_type='publicURL', **default_params) - def set_users_client(self, auth, creds, conf, endpoint_type, - default_params): + def get_service_client(self, service_name): + """Returns name of the service's client. + + :type service_name: string + :rtype: client object or None when the client doesn't exist + """ + if service_name == "image": + return self.images + elif service_name == "network": + # return whole ClientManager object because NetworkService + # currently needs to have an access to get_neutron/nova_client + # methods which are chosen according to neutron presence + return self + else: + return None + + def set_users_client(self, auth, identity_version, catalog_type, + endpoint_type, default_params): """Sets users client. :param auth: auth provider :type auth: auth.KeystoneV2AuthProvider (or V3) - :type creds: Credentials object - :type conf: TempestConf object + :type identity_version: string + :type catalog_type: string :type endpoint_type: string :type default_params: dict """ users_class = users_client.UsersClient - if "v3" in creds.identity_version: + if "v3" in identity_version: users_class = users_v3_client.UsersClient self.users = users_class( auth, - conf.get_defaulted('identity', 'catalog_type'), + catalog_type, self.identity_region, endpoint_type=endpoint_type, **default_params) - def set_roles_client(self, auth, creds, conf, endpoint_type, - default_params): + def set_roles_client(self, auth, identity_version, catalog_type, + endpoint_type, default_params): """Sets roles client. :param auth: auth provider :type auth: auth.KeystoneV2AuthProvider (or V3) - :type creds: Credentials object - :type conf: TempestConf object + :type identity_version: string + :type catalog_type: string :type endpoint_type: string :type default_params: dict """ roles_class = roles_client.RolesClient - if "v3" in creds.identity_version: + if "v3" in identity_version: roles_class = roles_v3_client.RolesClient self.roles = roles_class( auth, - conf.get_defaulted('identity', 'catalog_type'), + catalog_type, self.identity_region, endpoint_type=endpoint_type, **default_params) diff --git a/config_tempest/constants.py b/config_tempest/constants.py new file mode 100644 index 00000000..96ca3acf --- /dev/null +++ b/config_tempest/constants.py @@ -0,0 +1,67 @@ +# Copyright 2013, 2016, 2018 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging +import os + +LOG = logging.getLogger(__name__) + +# Get the current tempest workspace path +TEMPEST_WORKSPACE = os.getcwd() + +DEFAULTS_FILE = os.path.join(TEMPEST_WORKSPACE, "etc", + "default-overrides.conf") +DEFAULT_IMAGE = ("http://download.cirros-cloud.net/0.3.5/" + "cirros-0.3.5-x86_64-disk.img") +DEFAULT_IMAGE_FORMAT = 'qcow2' + +# services and their codenames +SERVICE_NAMES = { + 'baremetal': 'ironic', + 'compute': 'nova', + 'database': 'trove', + 'data-processing': 'sahara', + 'image': 'glance', + 'network': 'neutron', + 'object-store': 'swift', + 'orchestration': 'heat', + 'share': 'manila', + 'telemetry': 'ceilometer', + 'volume': 'cinder', + 'messaging': 'zaqar', + 'metric': 'gnocchi', + 'event': 'panko', +} + +# what API versions could the service have and should be enabled/disabled +# depending on whether they get discovered as supported. Services with only one +# version don't need to be here, neither do service versions that are not +# configurable in tempest.conf +SERVICE_VERSIONS = { + 'image': {'supported_versions': ['v1', 'v2'], 'catalog': 'image'}, + 'identity': {'supported_versions': ['v2', 'v3'], 'catalog': 'identity'}, + 'volume': {'supported_versions': ['v2', 'v3'], 'catalog': 'volumev3'} +} + +# Keep track of where the extensions are saved for that service. +# This is necessary because the configuration file is inconsistent - it uses +# different option names for service extension depending on the service. +SERVICE_EXTENSION_KEY = { + 'compute': 'api_extensions', + 'object-store': 'discoverable_apis', + 'network': 'api_extensions', + 'volume': 'api_extensions', + 'identity': 'api_extensions' +} diff --git a/config_tempest/credentials.py b/config_tempest/credentials.py index ed403ba9..3da32cc9 100644 --- a/config_tempest/credentials.py +++ b/config_tempest/credentials.py @@ -1,17 +1,17 @@ -# Copyright 2018 Red Hat, Inc. +# Copyright 2016, 2018 Red Hat, Inc. # All Rights Reserved. # -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# http://www.apache.org/licenses/LICENSE-2.0 # -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. from tempest.lib import auth @@ -35,6 +35,8 @@ class Credentials(object): self.password = self.get_credential('password') self.tenant_name = self.get_credential('tenant_name') self.identity_version = self._get_identity_version() + self.api_version = 3 if self.identity_version == "v3" else 2 + self.identity_region = self._conf.get_defaulted('identity', 'region') self.disable_ssl_certificate_validation = self._conf.get_defaulted( 'identity', 'disable_ssl_certificate_validation' diff --git a/config_tempest/flavors.py b/config_tempest/flavors.py new file mode 100644 index 00000000..19f9d22f --- /dev/null +++ b/config_tempest/flavors.py @@ -0,0 +1,91 @@ +# Copyright 2016 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from constants import LOG + + +class Flavors(object): + def __init__(self, client, allow_creation, conf): + """Init. + + :type client: FlavorsClient object from tempest lib + :type allow_creation: boolean + :type conf: TempestConf object + """ + self.client = client + self.allow_creation = allow_creation + self._conf = conf + + def create_tempest_flavors(self): + """Find or create flavors and set them in conf. + + If 'flavor_ref' and 'flavor_ref_alt' are specified in conf, it will + first try to find those - otherwise it will try finding or creating + 'm1.nano' and 'm1.micro' and overwrite those options in conf. + """ + # m1.nano flavor + flavor_id = None + if self._conf.has_option('compute', 'flavor_ref'): + flavor_id = self._conf.get('compute', 'flavor_ref') + flavor_id = self.find_or_create_flavor(flavor_id, 'm1.nano', ram=64) + self._conf.set('compute', 'flavor_ref', flavor_id) + + # m1.micro flavor + alt_flavor_id = None + if self._conf.has_option('compute', 'flavor_ref_alt'): + alt_flavor_id = self._conf.get('compute', 'flavor_ref_alt') + alt_flavor_id = self.find_or_create_flavor(alt_flavor_id, 'm1.micro', + ram=128) + self._conf.set('compute', 'flavor_ref_alt', alt_flavor_id) + + def find_or_create_flavor(self, flavor_id, flavor_name, + ram=64, vcpus=1, disk=0): + """Try finding flavor by ID or name, create if not found. + + :param flavor_id: first try finding the flavor by this + :param flavor_name: find by this if it was not found by ID, create new + flavor with this name if not found at allCLIENT_MOCK + :param ram: memory of created flavor in MB + :param vcpus: number of VCPUs for the flavor + :param disk: size of disk for flavor in GB + """ + flavor = None + flavors = self.client.list_flavors()['flavors'] + # try finding it by the ID first + if flavor_id: + found = [f for f in flavors if f['id'] == flavor_id] + if found: + flavor = found[0] + # if not found, try finding it by name + if flavor_name and not flavor: + found = [f for f in flavors if f['name'] == flavor_name] + if found: + flavor = found[0] + + if not flavor and not self.allow_creation: + raise Exception("Flavor '%s' not found, but resource creation" + " isn't allowed. Either use '--create' or provide" + " an existing flavor" % flavor_name) + + if not flavor: + LOG.info("Creating flavor '%s'", flavor_name) + flavor = self.client.create_flavor(name=flavor_name, + ram=ram, vcpus=vcpus, + disk=disk, id=None) + return flavor['flavor']['id'] + else: + LOG.info("(no change) Found flavor '%s'", flavor['name']) + + return flavor['id'] diff --git a/config_tempest/main.py b/config_tempest/main.py index 56c11f7f..913fb6f4 100755 --- a/config_tempest/main.py +++ b/config_tempest/main.py @@ -1,17 +1,17 @@ # Copyright 2016 Red Hat, Inc. # All Rights Reserved. # -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# http://www.apache.org/licenses/LICENSE-2.0 # -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. """ This script will generate the etc/tempest.conf file by applying a series of specified options in the following order: @@ -36,71 +36,26 @@ https://docs.openstack.org/developer/os-client-config/ obtained by querying the cloud. """ -import api_discovery import argparse import ConfigParser import logging import os -import shutil import sys -import urllib2 from clients import ClientManager +import constants as C +from constants import LOG from credentials import Credentials +from flavors import Flavors import os_client_config from oslo_config import cfg -from tempest.lib import exceptions +from services import boto +from services import ceilometer +from services.horizon import configure_horizon +from services.services import Services +from services import volume import tempest_conf - -LOG = logging.getLogger(__name__) - -# Get the current tempest workspace path -TEMPEST_WORKSPACE = os.getcwd() - -DEFAULTS_FILE = os.path.join(TEMPEST_WORKSPACE, "etc", - "default-overrides.conf") -DEFAULT_IMAGE = ("http://download.cirros-cloud.net/0.3.5/" - "cirros-0.3.5-x86_64-disk.img") -DEFAULT_IMAGE_FORMAT = 'qcow2' - -# services and their codenames -SERVICE_NAMES = { - 'baremetal': 'ironic', - 'compute': 'nova', - 'database': 'trove', - 'data-processing': 'sahara', - 'image': 'glance', - 'network': 'neutron', - 'object-store': 'swift', - 'orchestration': 'heat', - 'share': 'manila', - 'telemetry': 'ceilometer', - 'volume': 'cinder', - 'messaging': 'zaqar', - 'metric': 'gnocchi', - 'event': 'panko', -} - -# what API versions could the service have and should be enabled/disabled -# depending on whether they get discovered as supported. Services with only one -# version don't need to be here, neither do service versions that are not -# configurable in tempest.conf -SERVICE_VERSIONS = { - 'image': {'supported_versions': ['v1', 'v2'], 'catalog': 'image'}, - 'identity': {'supported_versions': ['v2', 'v3'], 'catalog': 'identity'}, - 'volume': {'supported_versions': ['v2', 'v3'], 'catalog': 'volumev3'} -} - -# Keep track of where the extensions are saved for that service. -# This is necessary because the configuration file is inconsistent - it uses -# different option names for service extension depending on the service. -SERVICE_EXTENSION_KEY = { - 'compute': 'api_extensions', - 'object-store': 'discoverable_apis', - 'network': 'api_extensions', - 'volume': 'api_extensions', - 'identity': 'api_extensions' -} +from users import Users def set_logging(debug, verbose): @@ -154,9 +109,9 @@ def set_options(conf, deployer_input, non_admin, overrides=[], :param cloud_creds: Cloud credentials from client's config :type cloud_creds: dict """ - if os.path.isfile(DEFAULTS_FILE): - LOG.info("Reading defaults from file '%s'", DEFAULTS_FILE) - conf.read(DEFAULTS_FILE) + if os.path.isfile(C.DEFAULTS_FILE): + LOG.info("Reading defaults from file '%s'", C.DEFAULTS_FILE) + conf.read(C.DEFAULTS_FILE) if deployer_input and os.path.isfile(deployer_input): read_deployer_input(deployer_input, conf) @@ -192,6 +147,14 @@ def set_options(conf, deployer_input, non_admin, overrides=[], for section, key, value in overrides: conf.set(section, key, value, priority=True) + uri = conf.get("identity", "uri") + if "v3" in uri: + conf.set("identity", "auth_version", "v3") + conf.set("identity", "uri_v3", uri) + else: + # TODO(arxcruz) make a check if v3 is enabled + conf.set("identity", "uri_v3", uri.replace("v2.0", "v3")) + def parse_arguments(): cloud_config = os_client_config.OpenStackConfig() @@ -223,14 +186,14 @@ def parse_arguments(): help='Run without admin creds') parser.add_argument('--test-accounts', default=None, metavar='PATH', help='Use accounts from accounts.yaml') - parser.add_argument('--image-disk-format', default=DEFAULT_IMAGE_FORMAT, + parser.add_argument('--image-disk-format', default=C.DEFAULT_IMAGE_FORMAT, help="""a format of an image to be uploaded to glance. - Default is '%s'""" % DEFAULT_IMAGE_FORMAT) - parser.add_argument('--image', default=DEFAULT_IMAGE, + Default is '%s'""" % C.DEFAULT_IMAGE_FORMAT) + parser.add_argument('--image', default=C.DEFAULT_IMAGE, help="""an image to be uploaded to glance. The name of the image is the leaf name of the path which can be either a filename or url. Default is - '%s'""" % DEFAULT_IMAGE) + '%s'""" % C.DEFAULT_IMAGE) parser.add_argument('--network-id', help="""The ID of an existing network in our openstack instance with external connectivity""") @@ -247,7 +210,6 @@ def parse_arguments(): " together, since creating" " resources requires" " admin rights") args.overrides = parse_overrides(args.overrides) - args.remove = parse_values_to_remove(args.remove) cloud = cloud_config.get_one_cloud(argparse=args) return cloud @@ -333,451 +295,9 @@ def set_cloud_config_values(non_admin, cloud_creds, conf): 'Could not load some identity options from cloud config file') -def create_tempest_users(tenants_client, roles_client, users_client, conf, - services): - """Create users necessary for Tempest if they don't exist already.""" - create_user_with_tenant(tenants_client, users_client, - conf.get('identity', 'username'), - conf.get('identity', 'password'), - conf.get('identity', 'tenant_name')) - - username = conf.get_defaulted('auth', 'admin_username') - if username is None: - username = conf.get_defaulted('identity', 'admin_username') - give_role_to_user(tenants_client, roles_client, users_client, - username, - conf.get('identity', 'tenant_name'), role_name='admin') - - # Prior to juno, and with earlier juno defaults, users needed to have - # the heat_stack_owner role to use heat stack apis. We assign that role - # to the user if the role is present. - if 'orchestration' in services: - give_role_to_user(tenants_client, roles_client, users_client, - conf.get('identity', 'username'), - conf.get('identity', 'tenant_name'), - role_name='heat_stack_owner', - role_required=False) - - create_user_with_tenant(tenants_client, users_client, - conf.get('identity', 'alt_username'), - conf.get('identity', 'alt_password'), - conf.get('identity', 'alt_tenant_name')) - - -def give_role_to_user(tenants_client, roles_client, users_client, username, - tenant_name, role_name, role_required=True): - """Give the user a role in the project (tenant).""", - tenant_id = tenants_client.get_project_by_name(tenant_name)['id'] - users = users_client.list_users() - user_ids = [u['id'] for u in users['users'] if u['name'] == username] - user_id = user_ids[0] - roles = roles_client.list_roles() - role_ids = [r['id'] for r in roles['roles'] if r['name'] == role_name] - if not role_ids: - if role_required: - raise Exception("required role %s not found" % role_name) - LOG.debug("%s role not required", role_name) - return - role_id = role_ids[0] - try: - roles_client.create_user_role_on_project(tenant_id, user_id, role_id) - LOG.debug("User '%s' was given the '%s' role in project '%s'", - username, role_name, tenant_name) - except exceptions.Conflict: - LOG.debug("(no change) User '%s' already has the '%s' role in" - " project '%s'", username, role_name, tenant_name) - - -def create_user_with_tenant(tenants_client, users_client, username, - password, tenant_name): - """Create a user and a tenant if it doesn't exist.""" - - LOG.info("Creating user '%s' with tenant '%s' and password '%s'", - username, tenant_name, password) - tenant_description = "Tenant for Tempest %s user" % username - email = "%s@test.com" % username - # create a tenant - try: - tenants_client.create_project(name=tenant_name, - description=tenant_description) - except exceptions.Conflict: - LOG.info("(no change) Tenant '%s' already exists", tenant_name) - - tenant_id = tenants_client.get_project_by_name(tenant_name)['id'] - - # create a user - try: - users_client.create_user(**{'name': username, 'password': password, - 'tenantId': tenant_id, 'email': email}) - except exceptions.Conflict: - LOG.info("User '%s' already exists.", username) - - -def create_tempest_flavors(client, conf, allow_creation): - """Find or create flavors 'm1.nano' and 'm1.micro' and set them in conf. - - If 'flavor_ref' and 'flavor_ref_alt' are specified in conf, it will first - try to find those - otherwise it will try finding or creating 'm1.nano' and - 'm1.micro' and overwrite those options in conf. - - :param allow_creation: if False, fail if flavors were not found - """ - # m1.nano flavor - flavor_id = None - if conf.has_option('compute', 'flavor_ref'): - flavor_id = conf.get('compute', 'flavor_ref') - flavor_id = find_or_create_flavor(client, - flavor_id, 'm1.nano', - allow_creation, ram=64) - conf.set('compute', 'flavor_ref', flavor_id) - - # m1.micro flavor - alt_flavor_id = None - if conf.has_option('compute', 'flavor_ref_alt'): - alt_flavor_id = conf.get('compute', 'flavor_ref_alt') - alt_flavor_id = find_or_create_flavor(client, - alt_flavor_id, 'm1.micro', - allow_creation, ram=128) - conf.set('compute', 'flavor_ref_alt', alt_flavor_id) - - -def find_or_create_flavor(client, flavor_id, flavor_name, - allow_creation, ram=64, vcpus=1, disk=0): - """Try finding flavor by ID or name, create if not found. - - :param flavor_id: first try finding the flavor by this - :param flavor_name: find by this if it was not found by ID, create new - flavor with this name if not found at all - :param allow_creation: if False, fail if flavors were not found - :param ram: memory of created flavor in MB - :param vcpus: number of VCPUs for the flavor - :param disk: size of disk for flavor in GB - """ - flavor = None - flavors = client.list_flavors()['flavors'] - # try finding it by the ID first - if flavor_id: - found = [f for f in flavors if f['id'] == flavor_id] - if found: - flavor = found[0] - # if not found previously, try finding it by name - if flavor_name and not flavor: - found = [f for f in flavors if f['name'] == flavor_name] - if found: - flavor = found[0] - - if not flavor and not allow_creation: - raise Exception("Flavor '%s' not found, but resource creation" - " isn't allowed. Either use '--create' or provide" - " an existing flavor" % flavor_name) - - if not flavor: - LOG.info("Creating flavor '%s'", flavor_name) - flavor = client.create_flavor(name=flavor_name, - ram=ram, vcpus=vcpus, - disk=disk, id=None) - return flavor['flavor']['id'] - else: - LOG.info("(no change) Found flavor '%s'", flavor['name']) - - return flavor['id'] - - -def create_tempest_images(client, conf, image_path, allow_creation, - disk_format): - img_path = os.path.join(conf.get("scenario", "img_dir"), - os.path.basename(image_path)) - name = image_path[image_path.rfind('/') + 1:] - conf.set('scenario', 'img_file', name) - alt_name = name + "_alt" - image_id = None - if conf.has_option('compute', 'image_ref'): - image_id = conf.get('compute', 'image_ref') - image_id = find_or_upload_image(client, - image_id, name, allow_creation, - image_source=image_path, - image_dest=img_path, - disk_format=disk_format) - alt_image_id = None - if conf.has_option('compute', 'image_ref_alt'): - alt_image_id = conf.get('compute', 'image_ref_alt') - alt_image_id = find_or_upload_image(client, - alt_image_id, alt_name, allow_creation, - image_source=image_path, - image_dest=img_path, - disk_format=disk_format) - - conf.set('compute', 'image_ref', image_id) - conf.set('compute', 'image_ref_alt', alt_image_id) - - -def check_ceilometer_service(client, conf, services): - try: - services = client.list_services(**{'type': 'metering'}) - except exceptions.Forbidden: - LOG.warning("User has no permissions to list services - " - "metering service can't be discovered.") - return - if services and len(services['services']): - metering = services['services'][0] - if 'ceilometer' in metering['name'] and metering['enabled']: - conf.set('service_available', 'ceilometer', 'True') - - -def check_volume_backup_service(client, conf, services): - """Verify if the cinder backup service is enabled""" - if 'volumev3' not in services: - LOG.info("No volume service found, skipping backup service check") - return - try: - params = {'binary': 'cinder-backup'} - backup_service = client.list_services(**params) - except exceptions.Forbidden: - LOG.warning("User has no permissions to list services - " - "cinder-backup service can't be discovered.") - return - - if backup_service: - # We only set backup to false if the service isn't running otherwise we - # keep the default value - service = backup_service['services'] - if not service or service[0]['state'] == 'down': - conf.set('volume-feature-enabled', 'backup', 'False') - - -def find_or_upload_image(client, image_id, image_name, allow_creation, - image_source='', image_dest='', disk_format=''): - image = _find_image(client, image_id, image_name) - if not image and not allow_creation: - raise Exception("Image '%s' not found, but resource creation" - " isn't allowed. Either use '--create' or provide" - " an existing image_ref" % image_name) - - if image: - LOG.info("(no change) Found image '%s'", image['name']) - path = os.path.abspath(image_dest) - if not os.path.isfile(path): - _download_image(client, image['id'], path) - else: - LOG.info("Creating image '%s'", image_name) - if image_source.startswith("http:") or \ - image_source.startswith("https:"): - _download_file(image_source, image_dest) - else: - shutil.copyfile(image_source, image_dest) - image = _upload_image(client, image_name, image_dest, disk_format) - return image['id'] - - -def create_tempest_networks(clients, conf, has_neutron, public_network_id): - label = None - public_network_name = None - # TODO(tkammer): separate logic to different func of Nova network - # vs Neutron - if has_neutron: - client = clients.get_neutron_client() - - # if user supplied the network we should use - if public_network_id: - LOG.info("Looking for existing network id: {0}" - "".format(public_network_id)) - - # check if network exists - network_list = client.list_networks() - for network in network_list['networks']: - if network['id'] == public_network_id: - public_network_name = network['name'] - break - else: - raise ValueError('provided network id: {0} was not found.' - ''.format(public_network_id)) - - # no network id provided, try to auto discover a public network - else: - LOG.info("No network supplied, trying auto discover for network") - network_list = client.list_networks() - for network in network_list['networks']: - if network['router:external'] and network['subnets']: - LOG.info("Found network, using: {0}".format(network['id'])) - public_network_id = network['id'] - public_network_name = network['name'] - break - - # Couldn't find an existing external network - else: - LOG.error("No external networks found. " - "Please note that any test that relies on external " - "connectivity would most likely fail.") - - if public_network_id is not None: - conf.set('network', 'public_network_id', public_network_id) - if public_network_name is not None: - conf.set('network', 'floating_network_name', public_network_name) - - else: - client = clients.get_nova_net_client() - networks = client.list_networks() - if networks: - label = networks['networks'][0]['label'] - - if label: - conf.set('compute', 'fixed_network_name', label) - elif not has_neutron: - raise Exception('fixed_network_name could not be discovered and' - ' must be specified') - - -def configure_keystone_feature_flags(conf, services): - """Set keystone feature flags based upon version ID.""" - supported_versions = services.get('identity', {}).get('versions', []) - if len(supported_versions) <= 1: - return - for version in supported_versions: - major, minor = version.split('.')[:2] - # Enable the domain specific roles feature flag. For more information, - # see https://developer.openstack.org/api-ref/identity/v3 - if major == 'v3' and int(minor) >= 6: - conf.set('identity-feature-enabled', - 'forbid_global_implied_dsr', - 'True') - - -def configure_boto(conf, services): - """Set boto URLs based on discovered APIs.""" - if 'ec2' in services: - conf.set('boto', 'ec2_url', services['ec2']['url']) - if 's3' in services: - conf.set('boto', 's3_url', services['s3']['url']) - - -def configure_horizon(conf): - """Derive the horizon URIs from the identity's URI.""" - uri = conf.get('identity', 'uri') - u = urllib2.urlparse.urlparse(uri) - base = '%s://%s%s' % (u.scheme, u.netloc.replace( - ':' + str(u.port), ''), '/dashboard') - assert base.startswith('http:') or base.startswith('https:') - has_horizon = True - try: - urllib2.urlopen(base) - except urllib2.URLError: - has_horizon = False - conf.set('service_available', 'horizon', str(has_horizon)) - conf.set('dashboard', 'dashboard_url', base + '/') - conf.set('dashboard', 'login_url', base + '/auth/login/') - - -def configure_discovered_services(conf, services): - """Set service availability and supported extensions and versions. - - Set True/False per service in the [service_available] section of `conf` - depending of wheter it is in services. In the [-feature-enabled] - section, set extensions and versions found in `services`. - - :param conf: ConfigParser configuration - :param services: dictionary of discovered services - expects each service - to have a dictionary containing 'extensions' and 'versions' keys - """ - # check if volume service is disabled - if conf.has_section('services') and conf.has_option('services', 'volume'): - if not conf.getboolean('services', 'volume'): - SERVICE_NAMES.pop('volume') - SERVICE_VERSIONS.pop('volume') - # set service availability - for service, codename in SERVICE_NAMES.iteritems(): - # ceilometer is still transitioning from metering to telemetry - if service == 'telemetry' and 'metering' in services: - service = 'metering' - conf.set('service_available', codename, str(service in services)) - - # TODO(arxcruz): Remove this once/if we get the following reviews merged - # in all branches supported by tempestconf, or once/if tempestconf do not - # support anymore the OpenStack release where those patches are not - # available. - # https://review.openstack.org/#/c/492526/ - # https://review.openstack.org/#/c/492525/ - - if 'alarming' in services: - conf.set('service_available', 'aodh', 'True') - conf.set('service_available', 'aodh_plugin', 'True') - - # set supported API versions for services with more of them - for service, service_info in SERVICE_VERSIONS.iteritems(): - supported_versions = services.get( - service_info['catalog'], {}).get('versions', []) - section = service + '-feature-enabled' - for version in service_info['supported_versions']: - is_supported = any(version in item - for item in supported_versions) - conf.set(section, 'api_' + version, str(is_supported)) - - # set service extensions - keystone_v3_support = conf.get('identity-feature-enabled', 'api_v3') - for service, ext_key in SERVICE_EXTENSION_KEY.iteritems(): - if service in services: - extensions = ','.join(services[service].get('extensions', "")) - if service == 'object-store': - # tempest.conf is inconsistent and uses 'object-store' for the - # catalog name but 'object-storage-feature-enabled' - service = 'object-storage' - elif service == 'identity' and keystone_v3_support: - identity_v3_ext = api_discovery.get_identity_v3_extensions( - conf.get("identity", "uri_v3")) - extensions = list(set(extensions.split(',') + identity_v3_ext)) - extensions = ','.join(extensions) - conf.set(service + '-feature-enabled', ext_key, extensions) - - -def _download_file(url, destination): - if os.path.exists(destination): - LOG.info("Image '%s' already fetched to '%s'.", url, destination) - return - LOG.info("Downloading '%s' and saving as '%s'", url, destination) - f = urllib2.urlopen(url) - data = f.read() - with open(destination, "wb") as dest: - dest.write(data) - - -def _download_image(client, id, path): - """Download file from glance.""" - LOG.info("Downloading image %s to %s", id, path) - body = client.show_image_file(id) - LOG.debug(type(body.data)) - with open(path, 'wb') as out: - out.write(body.data) - - -def _upload_image(client, name, path, disk_format): - """Upload image file from `path` into Glance with `name.""" - LOG.info("Uploading image '%s' from '%s'", name, os.path.abspath(path)) - - with open(path) as data: - image = client.create_image(name=name, - disk_format=disk_format, - container_format='bare', - visibility="public") - client.store_image_file(image['id'], data) - return image - - -def _find_image(client, image_id, image_name): - """Find image by ID or name (the image client doesn't have this).""" - if image_id: - try: - return client.show_image(image_id) - except exceptions.NotFound: - pass - found = filter(lambda x: x['name'] == image_name, - client.list_images()['images']) - if found: - return found[0] - else: - return None - - def main(): args = parse_arguments() + args.remove = parse_values_to_remove(args.remove) set_logging(args.debug, args.verbose) conf = tempest_conf.TempestConf() @@ -785,46 +305,35 @@ def main(): set_options(conf, args.deployer_input, args.non_admin, args.overrides, args.test_accounts, cloud_creds) - uri = conf.get("identity", "uri") - api_version = 2 - if "v3" in uri: - api_version = 3 - conf.set("identity", "auth_version", "v3") - conf.set("identity", "uri_v3", uri) - else: - # TODO(arxcruz) make a check if v3 is enabled - conf.set("identity", "uri_v3", uri.replace("v2.0", "v3")) credentials = Credentials(conf, not args.non_admin) clients = ClientManager(conf, credentials) - swift_discover = conf.get_defaulted('object-storage-feature-enabled', - 'discoverability') - services = api_discovery.discover( - clients.auth_provider, - clients.identity_region, - object_store_discovery=conf.get_bool_value(swift_discover), - api_version=api_version, - disable_ssl_certificate_validation=conf.get_defaulted( - 'identity', - 'disable_ssl_certificate_validation' - ) - ) + services = Services(clients, conf, credentials) + if args.create and args.test_accounts is None: - create_tempest_users(clients.tenants, clients.roles, clients.users, - conf, services) - create_tempest_flavors(clients.flavors, conf, args.create) - create_tempest_images(clients.images, conf, args.image, args.create, - args.image_disk_format) - has_neutron = "network" in services + users = Users(clients.tenants, clients.roles, clients.users, conf) + users.create_tempest_users(services.is_service('orchestration')) + flavors = Flavors(clients.flavors, args.create, conf) + flavors.create_tempest_flavors() - LOG.info("Setting up network") - LOG.debug("Is neutron present: {0}".format(has_neutron)) - create_tempest_networks(clients, conf, has_neutron, args.network_id) + image = services.get_service('image') + image.set_image_preferences(args.create, args.image, + args.image_disk_format) + image.create_tempest_images(conf) - configure_discovered_services(conf, services) - check_volume_backup_service(clients.volume_service, conf, services) - check_ceilometer_service(clients.service_client, conf, services) - configure_boto(conf, services) - configure_keystone_feature_flags(conf, services) + has_neutron = services.is_service("network") + network = services.get_service("network") + network.create_tempest_networks(has_neutron, conf, args.network_id) + + services.set_service_availability() + services.set_supported_api_versions() + services.set_service_extensions() + volume.check_volume_backup_service(conf, clients.volume_client, + services.is_service("volumev3")) + ceilometer.check_ceilometer_service(conf, clients.service_client) + boto.configure_boto(conf, + s3_service=services.get_service("s3")) + identity = services.get_service('identity') + identity.configure_keystone_feature_flags(conf) configure_horizon(conf) # remove all unwanted values if were specified diff --git a/config_tempest/services/__init__.py b/config_tempest/services/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/config_tempest/services/base.py b/config_tempest/services/base.py new file mode 100644 index 00000000..ff130c0f --- /dev/null +++ b/config_tempest/services/base.py @@ -0,0 +1,97 @@ +# Copyright 2013 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import json +import re +import urllib3 +import urlparse + +from config_tempest.constants import LOG +MULTIPLE_SLASH = re.compile(r'/+') + + +class ServiceError(Exception): + pass + + +class Service(object): + def __init__(self, name, service_url, token, disable_ssl_validation, + client=None): + self.name = name + self.service_url = service_url + self.headers = {'Accept': 'application/json', 'X-Auth-Token': token} + self.disable_ssl_validation = disable_ssl_validation + self.client = client + + self.extensions = [] + self.versions = [] + + def do_get(self, url, top_level=False, top_level_path=""): + parts = list(urlparse.urlparse(url)) + # 2 is the path offset + if top_level: + parts[2] = '/' + top_level_path + + parts[2] = MULTIPLE_SLASH.sub('/', parts[2]) + url = urlparse.urlunparse(parts) + + try: + if self.disable_ssl_validation: + urllib3.disable_warnings() + http = urllib3.PoolManager(cert_reqs='CERT_NONE') + else: + http = urllib3.PoolManager() + r = http.request('GET', url, headers=self.headers) + except Exception as e: + LOG.error("Request on service '%s' with url '%s' failed", + (self.name, url)) + raise e + if r.status >= 400: + raise ServiceError("Request on service '%s' with url '%s' failed" + " with code %d" % (self.name, url, r.status)) + return r.data + + def set_extensions(self): + self.extensions = [] + + def set_versions(self): + self.versions = [] + + def get_extensions(self): + return self.extensions + + def get_versions(self): + return self.versions + + +class VersionedService(Service): + def set_versions(self, top_level=True): + body = self.do_get(self.service_url, top_level=top_level) + body = json.loads(body) + self.versions = self.deserialize_versions(body) + + def deserialize_versions(self, body): + return map(lambda x: x['id'], body['versions']) + + def no_port_cut_url(self): + # if there is no port defined, cut the url from version to the end + u = urllib3.util.parse_url(self.service_url) + url = self.service_url + if u.port is None: + found = re.findall(r'v\d', url) + if len(found) > 0: + index = url.index(found[0]) + url = self.service_url[:index] + return (url, u.port is not None) diff --git a/config_tempest/services/boto.py b/config_tempest/services/boto.py new file mode 100644 index 00000000..ebe04cba --- /dev/null +++ b/config_tempest/services/boto.py @@ -0,0 +1,26 @@ +# Copyright 2016 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +def configure_boto(conf, ec2_service=None, s3_service=None): + """Set boto URLs based on discovered APIs. + + :type ec2_service: config_tempest.services.base.Service + :type s3_service: config_tempest.services.base.Service + """ + if ec2_service: + conf.set('boto', 'ec2_url', ec2_service.service_url) + if s3_service: + conf.set('boto', 's3_url', s3_service.service_url) diff --git a/config_tempest/services/ceilometer.py b/config_tempest/services/ceilometer.py new file mode 100644 index 00000000..a47d3798 --- /dev/null +++ b/config_tempest/services/ceilometer.py @@ -0,0 +1,36 @@ +# Copyright 2016 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import config_tempest.constants as C +from tempest.lib import exceptions + + +def check_ceilometer_service(conf, service_client): + """If a metering service is available, set it to conf + + :type conf: TempestConf object + :type service_client: Tempest's identity.v3.services_client.ServicesClient + """ + try: + params = {'type': 'metering'} + services = service_client.list_services(**params) + except exceptions.Forbidden: + C.LOG.warning("User has no permissions to list services - " + "metering service can't be discovered.") + return + if services and len(services['services']): + metering = services['services'][0] + if 'ceilometer' in metering['name'] and metering['enabled']: + conf.set('service_available', 'ceilometer', 'True') diff --git a/config_tempest/services/compute.py b/config_tempest/services/compute.py new file mode 100644 index 00000000..5723bd6f --- /dev/null +++ b/config_tempest/services/compute.py @@ -0,0 +1,30 @@ +# Copyright 2013 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from base import VersionedService +import json + + +class ComputeService(VersionedService): + def set_extensions(self): + body = self.do_get(self.service_url + '/extensions') + body = json.loads(body) + self.extensions = map(lambda x: x['alias'], body['extensions']) + + def set_versions(self): + url, top_level = self.no_port_cut_url() + body = self.do_get(url, top_level=top_level) + body = json.loads(body) + self.versions = self.deserialize_versions(body) diff --git a/config_tempest/services/horizon.py b/config_tempest/services/horizon.py new file mode 100644 index 00000000..330ad762 --- /dev/null +++ b/config_tempest/services/horizon.py @@ -0,0 +1,33 @@ +# Copyright 2016 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import urllib2 + + +def configure_horizon(conf): + """Derive the horizon URIs from the identity's URI.""" + uri = conf.get('identity', 'uri') + u = urllib2.urlparse.urlparse(uri) + base = '%s://%s%s' % (u.scheme, u.netloc.replace( + ':' + str(u.port), ''), '/dashboard') + assert base.startswith('http:') or base.startswith('https:') + has_horizon = True + try: + urllib2.urlopen(base) + except urllib2.URLError: + has_horizon = False + conf.set('service_available', 'horizon', str(has_horizon)) + conf.set('dashboard', 'dashboard_url', base + '/') + conf.set('dashboard', 'login_url', base + '/auth/login/') diff --git a/config_tempest/services/identity.py b/config_tempest/services/identity.py new file mode 100644 index 00000000..6706e8ce --- /dev/null +++ b/config_tempest/services/identity.py @@ -0,0 +1,112 @@ +# Copyright 2013 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import json +import requests +import urlparse + +from base import VersionedService +from config_tempest.constants import LOG + + +class IdentityService(VersionedService): + def __init__(self, name, service_url, token, disable_ssl_validation, + client=None): + super(IdentityService, self).__init__( + name, service_url, token, disable_ssl_validation, client) + self.extensions_v3 = [] + version = '' + if 'v2' in self.service_url: + version = '/v2.0' + url_parse = urlparse.urlparse(self.service_url) + self.service_url = '{}://{}{}'.format(url_parse.scheme, + url_parse.netloc, version) + + def set_extensions(self): + if 'v2' in self.service_url: + body = self.do_get(self.service_url + '/extensions') + body = json.loads(body) + values = body['extensions']['values'] + self.extensions = map(lambda x: x['alias'], values) + return + # Keystone api changed in v3, the concept of extensions changed. Right + # now, all the existing extensions are part of keystone core api, so, + # there's no longer the /extensions endpoint. The extensions that are + # stable, are enabled by default, the ones marked as experimental are + # disabled by default. Checking the tempest source, there's no test + # pointing to extensions endpoint, so I am very confident that this + # will not be an issue. If so, we need to list all the /OS-XYZ + # extensions to identify what is enabled or not. This would be a manual + # check every time keystone change, add or delete an extension, so I + # rather prefer to set empty list here for now. + self.extensions = [] + + def set_identity_v3_extensions(self): + """Returns discovered identity v3 extensions + + As keystone V3 uses a JSON Home to store the extensions. + This method implements a different discovery method. + + :param keystone_v3_url: Keystone V3 auth url + :return: A list with the discovered extensions + """ + try: + r = requests.get(self.service_url, + verify=False, + headers={'Accept': 'application/json-home'}) + except requests.exceptions.RequestException as re: + LOG.error("Request on service '%s' with url '%s' failed", + 'identity', self.service_url) + raise re + ext_h = 'http://docs.openstack.org/api/openstack-identity/3/ext/' + res = [x for x in json.loads(r.content)['resources'].keys()] + ext = [ex for ex in res if 'ext' in ex] + ext = [str(e).replace(ext_h, '').split('/')[0] for e in ext] + self.extensions_v3 = list(set(ext)) + + def set_versions(self): + super(IdentityService, self).set_versions(top_level=False) + + def get_extensions(self): + all_ext_lst = self.extensions + self.extensions_v3 + return list(set(all_ext_lst)) + + def deserialize_versions(self, body): + try: + versions = [] + for v in body['versions']['values']: + # TripleO is in transition to v3 only, so the environment + # still returns v2 versions even though they're deprecated. + # Therefor pick only versions with stable status. + if v['status'] == 'stable': + versions.append(v['id']) + return versions + except KeyError: + return [body['version']['id']] + + def configure_keystone_feature_flags(self, conf): + """Set keystone feature flags based upon version ID.""" + supported_versions = self.get_versions() + if len(supported_versions) <= 1: + return + for version in supported_versions: + major, minor = version.split('.')[:2] + # Enable the domain specific roles feature flag. + # For more information see: + # https://developer.openstack.org/api-ref/identity/v3 + if major == 'v3' and int(minor) >= 6: + conf.set('identity-feature-enabled', + 'forbid_global_implied_dsr', + 'True') diff --git a/config_tempest/services/image.py b/config_tempest/services/image.py new file mode 100644 index 00000000..c9228306 --- /dev/null +++ b/config_tempest/services/image.py @@ -0,0 +1,168 @@ +# Copyright 2013 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import os +import shutil +import urllib2 + +from base import VersionedService +from config_tempest.constants import LOG +from tempest.lib import exceptions + + +class ImageService(VersionedService): + + def __init__(self, name, service_url, token, disable_ssl_validation, + client=None): + super(ImageService, self).__init__(name, service_url, token, + disable_ssl_validation, + client) + self.allow_creation = False + self.image_path = "" + self.disk_format = "" + + def set_image_preferences(self, allow_creation, image_path, disk_format): + """Sets image prefferences. + + :type allow_creation: boolean + :type image_path: string + :type disk_format: string + """ + self.allow_creation = allow_creation + self.image_path = image_path + self.disk_format = disk_format + + def set_versions(self): + super(ImageService, self).set_versions(top_level=False) + + def create_tempest_images(self, conf): + """Uploads an image to the glance. + + The method creates images specified in conf, if they're not created + already. Then it sets their IDs to the conf. + + :type conf: TempestConf object + """ + img_path = os.path.join(conf.get("scenario", "img_dir"), + os.path.basename(self.image_path)) + name = self.image_path[self.image_path.rfind('/') + 1:] + conf.set('scenario', 'img_file', name) + alt_name = name + "_alt" + image_id = None + if conf.has_option('compute', 'image_ref'): + image_id = conf.get('compute', 'image_ref') + image_id = self.find_or_upload_image(image_id, name, + image_source=self.image_path, + image_dest=img_path) + alt_image_id = None + if conf.has_option('compute', 'image_ref_alt'): + alt_image_id = conf.get('compute', 'image_ref_alt') + alt_image_id = self.find_or_upload_image(alt_image_id, alt_name, + image_source=self.image_path, + image_dest=img_path) + + conf.set('compute', 'image_ref', image_id) + conf.set('compute', 'image_ref_alt', alt_image_id) + + def find_or_upload_image(self, image_id, image_name, image_source='', + image_dest=''): + """If the image is not found, uploads it. + + :type image_id: string + :type image_name: string + :type image_source: string + :type image_dest: string + """ + image = self._find_image(image_id, image_name) + if not image and not self.allow_creation: + raise Exception("Image '%s' not found, but resource creation" + " isn't allowed. Either use '--create' or provide" + " an existing image_ref" % image_name) + + if image: + LOG.info("(no change) Found image '%s'", image['name']) + path = os.path.abspath(image_dest) + if not os.path.isfile(path): + self._download_image(image['id'], path) + else: + LOG.info("Creating image '%s'", image_name) + if image_source.startswith("http:") or \ + image_source.startswith("https:"): + self._download_file(image_source, image_dest) + else: + shutil.copyfile(image_source, image_dest) + image = self._upload_image(image_name, image_dest) + return image['id'] + + def _find_image(self, image_id, image_name): + """Find image by ID or name (the image client doesn't have this). + + :type image_id: string + :type image_name: string + """ + if image_id: + try: + return self.client.show_image(image_id) + except exceptions.NotFound: + pass + found = filter(lambda x: x['name'] == image_name, + self.client.list_images()['images']) + if found: + return found[0] + else: + return None + + def _upload_image(self, name, path): + """Upload image file from `path` into Glance with `name`. + + :type name: string + :type path: string + """ + LOG.info("Uploading image '%s' from '%s'", name, os.path.abspath(path)) + + with open(path) as data: + image = self.client.create_image(name=name, + disk_format=self.disk_format, + container_format='bare', + visibility="public") + self.client.store_image_file(image['id'], data) + return image + + def _download_image(self, id, path): + """Download image from glance. + + :type id: string + :type path: string + """ + LOG.info("Downloading image %s to %s", id, path) + body = self.client.show_image_file(id) + LOG.debug(type(body.data)) + with open(path, 'wb') as out: + out.write(body.data) + + def _download_file(self, url, destination): + """Downloads a file specified by `url` to `destination`. + + :type url: string + :type destination: string + """ + if os.path.exists(destination): + LOG.info("Image '%s' already fetched to '%s'.", url, destination) + return + LOG.info("Downloading '%s' and saving as '%s'", url, destination) + f = urllib2.urlopen(url) + data = f.read() + with open(destination, "wb") as dest: + dest.write(data) diff --git a/config_tempest/services/network.py b/config_tempest/services/network.py new file mode 100644 index 00000000..967ecbd7 --- /dev/null +++ b/config_tempest/services/network.py @@ -0,0 +1,90 @@ +# Copyright 2013, 2016 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import json + +from base import VersionedService +from config_tempest.constants import LOG + + +class NetworkService(VersionedService): + def set_extensions(self): + body = self.do_get(self.service_url + '/v2.0/extensions.json') + body = json.loads(body) + self.extensions = map(lambda x: x['alias'], body['extensions']) + + def create_tempest_networks(self, has_neutron, conf, network_id): + LOG.info("Setting up network") + LOG.debug("Is neutron present: {0}".format(has_neutron)) + if has_neutron: + self.client = self.client.get_neutron_client() + self.create_tempest_networks_neutron(conf, network_id) + else: + self.client = self.client.get_nova_net_client() + self.create_tempest_networks_nova(conf) + + def create_tempest_networks_neutron(self, conf, public_network_id): + self._public_network_name = None + self._public_network_id = public_network_id + # if user supplied the network we should use + if public_network_id: + self._supplied_network() + # no network id provided, try to auto discover a public network + else: + self._discover_network() + if self._public_network_id is not None: + conf.set('network', 'public_network_id', self._public_network_id) + if self._public_network_name is not None: + conf.set('network', 'floating_network_name', + self._public_network_name) + + def _supplied_network(self): + LOG.info("Looking for existing network id: {0}" + "".format(self._public_network_id)) + # check if network exists + network_list = self.client.list_networks() + for network in network_list['networks']: + if network['id'] == self._public_network_id: + self._public_network_name = network['name'] + break + else: + raise ValueError('provided network id: {0} was not found.' + ''.format(self._public_network_id)) + + def _discover_network(self): + LOG.info("No network supplied, trying auto discover for network") + network_list = self.client.list_networks() + for network in network_list['networks']: + if network['router:external'] and network['subnets']: + LOG.info("Found network, using: {0}".format(network['id'])) + self._public_network_id = network['id'] + self._public_network_name = network['name'] + break + + # Couldn't find an existing external network + else: + LOG.error("No external networks found. " + "Please note that any test that relies on external " + "connectivity would most likely fail.") + + def create_tempest_networks_nova(self, conf): + networks = self.client.list_networks() + if networks: + label = networks['networks'][0]['label'] + if label: + conf.set('compute', 'fixed_network_name', label) + else: + raise Exception('fixed_network_name could not be ' + 'discovered and must be specified') diff --git a/config_tempest/services/object_storage.py b/config_tempest/services/object_storage.py new file mode 100644 index 00000000..04c6f6a2 --- /dev/null +++ b/config_tempest/services/object_storage.py @@ -0,0 +1,31 @@ +# Copyright 2016 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import json + +from base import Service + + +class ObjectStorageService(Service): + def set_extensions(self, object_store_discovery=False): + if not object_store_discovery: + self.extensions = [] + elif 'v3' not in self.service_url: # it's not a v3 url + body = self.do_get(self.service_url, top_level=True, + top_level_path="info") + body = json.loads(body) + # Remove Swift general information from extensions list + body.pop('swift') + self.extensions = body.keys() diff --git a/config_tempest/services/services.py b/config_tempest/services/services.py new file mode 100644 index 00000000..b0afce9a --- /dev/null +++ b/config_tempest/services/services.py @@ -0,0 +1,204 @@ +# Copyright 2013, 2016, 2018 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import urlparse + +from base import Service +from compute import ComputeService +import config_tempest.constants as C +from identity import IdentityService +from image import ImageService +from network import NetworkService +from object_storage import ObjectStorageService +from volume import VolumeService + +service_dict = {'compute': ComputeService, + 'image': ImageService, + 'network': NetworkService, + 'object-store': ObjectStorageService, + 'volumev3': VolumeService, + 'identity': IdentityService} + + +class Services(object): + def __init__(self, clients, conf, creds): + self._clients = clients + self._conf = conf + self._creds = creds + swift_discover = conf.get_defaulted('object-storage-feature-enabled', + 'discoverability') + self._object_store_discovery = conf.get_bool_value(swift_discover) + self._ssl_validation = creds.disable_ssl_certificate_validation + self._region = clients.identity_region + self._services = [] + self.set_catalog_and_url() + + self.discover() + + def discover(self): + token, auth_data = self._clients.auth_provider.get_auth() + + for entry in auth_data[self.service_catalog]: + name = entry['type'] + ep = self.get_endpoints(entry) + + url = ep[self.public_url] + if 'identity' in url: + url = self.edit_identity_url(ep[self.public_url]) + + service_class = self.get_service_class(name) + service = service_class(name, url, token, self._ssl_validation, + self._clients.get_service_client(name)) + # discover extensions of the service + if name == 'object-store': + service.set_extensions(self._object_store_discovery) + else: + service.set_extensions() + # discover versions of the service + service.set_versions() + + self._services.append(service) + + def get_endpoints(self, entry): + for ep in entry['endpoints']: + if self._creds.api_version == 3: + if (ep['region'] == self._region and + ep['interface'] == 'public'): + return ep + else: + if ep['region'] == self._region: + return ep + try: + return entry['endpoints'][0] + except IndexError: + return [] + + def set_catalog_and_url(self): + if self._creds.api_version == 3: + self.service_catalog = 'catalog' + self.public_url = 'url' + else: + self.service_catalog = 'serviceCatalog' + self.public_url = 'publicURL' + + def edit_identity_url(self, url): + """A port and identity version are added to url if contains 'identity' + + :param url: url address of an endpoint + :type url: string + :rtype: string + """ + + # self._clients.auth_provider.auth_url stores identity.uri(_v3) value + # from TempestConf + port = urlparse.urlparse(self._clients.auth_provider.auth_url).port + if port is None: + port = "" + else: + port = ":" + str(port) + replace_text = port + "/identity/" + self._creds.identity_version + return url.replace("/identity", replace_text) + + def get_service_class(self, name): + """Returns class name by the service name + + :param name: Codename of a service + :type name: string + :return: Class name of the service + :rtype: string + """ + return service_dict.get(name, Service) + + def get_service(self, name): + """Finds and returns a service object + + :param name: Codename of a service + :type name: string + :return: Service object + """ + for service in self._services: + if service.name == name: + return service + return None + + def is_service(self, name): + """Returns true if a service is available, false otherwise + + :param name: Codename of a service + :type name: string + :rtype: boolean + """ + if self.get_service(name) is None: + return False + return True + + def set_service_availability(self): + # check if volume service is disabled + if self._conf.has_option('services', 'volume'): + if not self._conf.getboolean('services', 'volume'): + C.SERVICE_NAMES.pop('volume') + C.SERVICE_VERSIONS.pop('volume') + + for service, codename in C.SERVICE_NAMES.iteritems(): + # ceilometer is still transitioning from metering to telemetry + if service == 'telemetry' and self.is_service('metering'): + service = 'metering' + available = str(self.is_service(service)) + self._conf.set('service_available', codename, available) + + # TODO(arxcruz): Remove this once/if we get the following reviews + # merged in all branches supported by tempestconf, or once/if + # tempestconf do not support anymore the OpenStack release where + # those patches are not available. + # https://review.openstack.org/#/c/492526/ + # https://review.openstack.org/#/c/492525/ + + if self.is_service('alarming'): + self._conf.set('service_available', 'aodh', 'True') + self._conf.set('service_available', 'aodh_plugin', 'True') + + def set_supported_api_versions(self): + # set supported API versions for services with more of them + for service, service_info in C.SERVICE_VERSIONS.iteritems(): + service_object = self.get_service(service_info['catalog']) + if service_object is None: + supported_versions = [] + else: + supported_versions = service_object.get_versions() + section = service + '-feature-enabled' + for version in service_info['supported_versions']: + is_supported = any(version in item + for item in supported_versions) + self._conf.set(section, 'api_' + version, str(is_supported)) + + def set_service_extensions(self): + postfix = "-feature-enabled" + keystone_v3_support = self._conf.get('identity' + postfix, 'api_v3') + if keystone_v3_support: + self.get_service('identity').set_identity_v3_extensions() + + for service, ext_key in C.SERVICE_EXTENSION_KEY.iteritems(): + if not self.is_service(service): + continue + service_object = self.get_service(service) + if service_object is not None: + extensions = ','.join(service_object.get_extensions()) + + if service == 'object-store': + # tempest.conf is inconsistent and uses 'object-store' for + # the catalog name but 'object-storage-feature-enabled' + service = 'object-storage' + + self._conf.set(service + postfix, ext_key, extensions) diff --git a/config_tempest/services/volume.py b/config_tempest/services/volume.py new file mode 100644 index 00000000..e1e5d498 --- /dev/null +++ b/config_tempest/services/volume.py @@ -0,0 +1,55 @@ +# Copyright 2013, 2016 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import json + +from base import VersionedService +import config_tempest.constants as C +from tempest.lib import exceptions + + +class VolumeService(VersionedService): + def set_extensions(self): + body = self.do_get(self.service_url + '/extensions') + body = json.loads(body) + self.extensions = map(lambda x: x['alias'], body['extensions']) + + def set_versions(self): + url, top_level = self.no_port_cut_url() + body = self.do_get(url, top_level=top_level) + body = json.loads(body) + self.versions = self.deserialize_versions(body) + + +def check_volume_backup_service(conf, volume_client, is_volumev3): + """Verify if the cinder backup service is enabled""" + if not is_volumev3: + C.LOG.info("No volume service found, " + "skipping backup service check") + return + try: + params = {'binary': 'cinder-backup'} + is_backup = volume_client.list_services(**params) + except exceptions.Forbidden: + C.LOG.warning("User has no permissions to list services - " + "cinder-backup service can't be discovered.") + return + + if is_backup: + # We only set backup to false if the service isn't running + # otherwise we keep the default value + service = is_backup['services'] + if not service or service[0]['state'] == 'down': + conf.set('volume-feature-enabled', 'backup', 'False') diff --git a/config_tempest/tempest_conf.py b/config_tempest/tempest_conf.py index d6cff2b8..5b42e529 100644 --- a/config_tempest/tempest_conf.py +++ b/config_tempest/tempest_conf.py @@ -1,4 +1,4 @@ -# Copyright 2018 Red Hat, Inc. +# Copyright 2016, 2017 Red Hat, Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -14,14 +14,12 @@ # under the License. import ConfigParser -import logging import sys +from constants import LOG from oslo_config import cfg import tempest.config -LOG = logging.getLogger(__name__) - class TempestConf(ConfigParser.SafeConfigParser): # causes the config parser to preserve case of the options diff --git a/config_tempest/tests/base.py b/config_tempest/tests/base.py index e9732033..eefb7b72 100644 --- a/config_tempest/tests/base.py +++ b/config_tempest/tests/base.py @@ -20,7 +20,6 @@ import json import mock from oslotest import base -from config_tempest import api_discovery as api from config_tempest.clients import ClientManager from config_tempest.credentials import Credentials from config_tempest import tempest_conf @@ -83,6 +82,8 @@ class BaseServiceTest(base.BaseTestCase): """Test case base class for all api_discovery unit tests""" FAKE_TOKEN = "s6d5f45sdf4s564f4s6464sdfsd514" + FAKE_CLIENT_MOCK = 'config_tempest.tests.base.BaseServiceTest' + \ + '.FakeServiceClient' FAKE_HEADERS = { 'Accept': 'application/json', 'X-Auth-Token': FAKE_TOKEN } @@ -131,6 +132,14 @@ class BaseServiceTest(base.BaseTestCase): } } ) + FAKE_IDENTITY_VERSION = ( + { + 'version': { + 'status': 'stable', + 'id': 'v3.8', + } + } + ) FAKE_EXTENSIONS = ( { "extensions": [{ @@ -202,22 +211,54 @@ class BaseServiceTest(base.BaseTestCase): def __init__(self): self.content = json.dumps(self.FAKE_V3_EXTENSIONS) + class FakeServiceClient(object): + def __init__(self, services=None): + self.client = mock.Mock() + self.return_value = mock.Mock() + self.services = services + + def list_networks(self): + return self.return_value + + def list_services(self, **kwargs): + return self.services + def _fake_service_do_get_method(self, fake_data): - function2mock = 'config_tempest.api_discovery.Service.do_get' + function2mock = 'config_tempest.services.base.Service.do_get' do_get_output = json.dumps(fake_data) mocked_do_get = mock.Mock() mocked_do_get.return_value = do_get_output self.useFixture(MonkeyPatch(function2mock, mocked_do_get)) - def _test_get_service_class(self, service, cls): - resp = api.get_service_class(service) - self.assertEqual(resp, cls) - - def _get_extensions(self, service, expected_resp, fake_data): + def _set_get_extensions(self, service, expected_resp, fake_data): + # mock do_get response self._fake_service_do_get_method(fake_data) + # set the fake extensions + service.set_extensions() + # check if extensions were set + self.assertItemsEqual(service.extensions, expected_resp) + # check if get method returns the right data resp = service.get_extensions() self.assertItemsEqual(resp, expected_resp) + def _set_get_versions(self, service, expected_resp, fake_data): + # mock do_get response + self._fake_service_do_get_method(fake_data) + # set the fake versions + service.set_versions() + # check if versions were set + self.assertItemsEqual(service.versions, expected_resp) + # check if get method returns the right data + resp = service.get_versions() + self.assertItemsEqual(resp, expected_resp) + def _test_deserialize_versions(self, service, expected_resp, fake_data): resp = service.deserialize_versions(fake_data) self.assertItemsEqual(resp, expected_resp) + + def _assert_conf_get_not_raises(self, exc, section, value): + try: + self.conf.get(section, value) + except exc: + return + self.assertTrue(False) diff --git a/config_tempest/tests/services/__init__.py b/config_tempest/tests/services/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/config_tempest/tests/services/test_base.py b/config_tempest/tests/services/test_base.py new file mode 100644 index 00000000..02968fdf --- /dev/null +++ b/config_tempest/tests/services/test_base.py @@ -0,0 +1,105 @@ +# Copyright 2018 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import mock +import unittest + +from config_tempest.services.base import Service +from config_tempest.services.base import VersionedService +from config_tempest.tests.base import BaseServiceTest + + +class TestService(BaseServiceTest): + def setUp(self): + super(TestService, self).setUp() + self.Service = Service("ServiceName", + self.FAKE_URL, + self.FAKE_TOKEN, + disable_ssl_validation=False) + + def _mocked_do_get(self, mock_urllib3): + mock_http = mock_urllib3.PoolManager() + expected_resp = mock_http.request('GET', + self.FAKE_URL, + self.FAKE_HEADERS) + return expected_resp.data + + @unittest.skip('Failing due to Storyboard: 2001245') + @mock.patch('config_tempest.api_discovery.urllib3') + def test_do_get(self, mock_urllib3): + resp = self.Service.do_get(self.FAKE_URL) + expected_resp = self._mocked_do_get(mock_urllib3) + self.assertEqual(resp, expected_resp) + + def test_service_properties(self): + self.assertEqual(self.Service.name, "ServiceName") + self.assertEqual(self.Service.service_url, self.FAKE_URL) + self.assertEqual(self.Service.headers, self.FAKE_HEADERS) + self.assertEqual(self.Service.disable_ssl_validation, False) + self.assertEqual(self.Service.client, None) + self.assertEqual(self.Service.extensions, []) + self.assertEqual(self.Service.versions, []) + + def test_set_extensions(self): + self.Service.extensions = ['ext'] + self.Service.set_extensions() + self.assertEqual(self.Service.extensions, []) + + def test_set_versions(self): + self.Service.versions = ['ver'] + self.Service.set_versions() + self.assertEqual(self.Service.versions, []) + + def test_get_extensions(self): + self.Service.extensions = ['ext'] + self.assertEqual(self.Service.get_extensions(), ['ext']) + + def test_get_versions(self): + self.Service.versions = ['ver'] + self.assertEqual(self.Service.get_versions(), ['ver']) + + +class TestVersionedService(BaseServiceTest): + def setUp(self): + super(TestVersionedService, self).setUp() + self.Service = VersionedService("ServiceName", + self.FAKE_URL, + self.FAKE_TOKEN, + disable_ssl_validation=False) + + def test_set_get_versions(self): + expected_resp = ['v2.0', 'v2.1'] + self._fake_service_do_get_method(self.FAKE_VERSIONS) + self.Service.set_versions() + resp = self.Service.get_versions() + self.assertItemsEqual(resp, expected_resp) + self.assertItemsEqual(self.Service.versions, expected_resp) + + def test_deserialize_versions(self): + expected_resp = ['v2.0', 'v2.1'] + self._test_deserialize_versions(self.Service, + expected_resp, + self.FAKE_VERSIONS) + + def test_no_port_cut_url(self): + resp = self.Service.no_port_cut_url() + self.assertEqual(resp, (self.FAKE_URL, True)) + url = "http://10.200.16.10" + self.Service.service_url = url + resp = self.Service.no_port_cut_url() + self.assertEqual(resp, (self.Service.service_url, False)) + self.Service.service_url = url + "/v3/cut/it/off" + resp = self.Service.no_port_cut_url() + self.assertEqual(resp, (url + '/', False)) diff --git a/config_tempest/tests/services/test_boto.py b/config_tempest/tests/services/test_boto.py new file mode 100644 index 00000000..43928661 --- /dev/null +++ b/config_tempest/tests/services/test_boto.py @@ -0,0 +1,47 @@ +# Copyright 2018 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import ConfigParser + +from config_tempest.services import base +from config_tempest.services import boto +from config_tempest.tempest_conf import TempestConf +from config_tempest.tests.base import BaseServiceTest + + +class TestBotoService(BaseServiceTest): + def setUp(self): + super(TestBotoService, self).setUp() + self.conf = TempestConf() + self.es2 = base.Service("ec2", + self.FAKE_URL, + self.FAKE_TOKEN, + disable_ssl_validation=False) + self.s3 = base.Service("s3", + self.FAKE_URL, + self.FAKE_TOKEN, + disable_ssl_validation=False) + + def test_configure_boto(self): + boto.configure_boto(self.conf) + self._assert_conf_get_not_raises(ConfigParser.NoSectionError, + "boto", + "ec2_url") + self._assert_conf_get_not_raises(ConfigParser.NoSectionError, + "boto", + "s3_url") + boto.configure_boto(self.conf, self.es2, self.s3) + self.assertEqual(self.conf.get("boto", "ec2_url"), self.FAKE_URL) + self.assertEqual(self.conf.get("boto", "s3_url"), self.FAKE_URL) diff --git a/config_tempest/tests/services/test_ceilometer.py b/config_tempest/tests/services/test_ceilometer.py new file mode 100644 index 00000000..01480604 --- /dev/null +++ b/config_tempest/tests/services/test_ceilometer.py @@ -0,0 +1,46 @@ +# Copyright 2018 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import ConfigParser + +from config_tempest.services import ceilometer +from config_tempest.tempest_conf import TempestConf +from config_tempest.tests.base import BaseServiceTest + + +class TestCeilometerService(BaseServiceTest): + def setUp(self): + super(TestCeilometerService, self).setUp() + self.conf = TempestConf() + + def test_check_ceilometer_service(self): + client_service_mock = self.FakeServiceClient(services={}) + ceilometer.check_ceilometer_service(self.conf, client_service_mock) + + self._assert_conf_get_not_raises(ConfigParser.NoSectionError, + "service_available", + "ceilometer") + + client_service_mock = self.FakeServiceClient(services={ + 'services': [ + { + "name": "ceilometer", + "enabled": True + } + ] + }) + ceilometer.check_ceilometer_service(self.conf, client_service_mock) + self.assertEqual(self.conf.get('service_available', 'ceilometer'), + 'True') diff --git a/config_tempest/tests/services/test_compute.py b/config_tempest/tests/services/test_compute.py new file mode 100644 index 00000000..a2fbca05 --- /dev/null +++ b/config_tempest/tests/services/test_compute.py @@ -0,0 +1,34 @@ +# Copyright 2018 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from config_tempest.services.compute import ComputeService +from config_tempest.tests.base import BaseServiceTest + + +class TestComputeService(BaseServiceTest): + def setUp(self): + super(TestComputeService, self).setUp() + self.Service = ComputeService("ServiceName", + self.FAKE_URL, + self.FAKE_TOKEN, + disable_ssl_validation=False) + + def test_set_get_extensions(self): + exp_resp = ['NMN', 'OS-DCF'] + self._set_get_extensions(self.Service, exp_resp, self.FAKE_EXTENSIONS) + + def test_set_get_versions(self): + exp_resp = ['v2.0', 'v2.1'] + self._set_get_versions(self.Service, exp_resp, self.FAKE_VERSIONS) diff --git a/config_tempest/tests/services/test_horizon.py b/config_tempest/tests/services/test_horizon.py new file mode 100644 index 00000000..8b19a898 --- /dev/null +++ b/config_tempest/tests/services/test_horizon.py @@ -0,0 +1,48 @@ +# Copyright 2018 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from fixtures import MonkeyPatch +import mock + +from config_tempest.services import horizon +from config_tempest.tests.base import BaseConfigTempestTest + + +class TestConfigTempest(BaseConfigTempestTest): + + def setUp(self): + super(TestConfigTempest, self).setUp() + self.conf = self._get_conf("v2.0", "v3") + + def test_configure_horizon_ipv4(self): + mock_function = mock.Mock(return_value=True) + self.useFixture(MonkeyPatch('urllib2.urlopen', mock_function)) + horizon.configure_horizon(self.conf) + self.assertEqual(self.conf.get('service_available', 'horizon'), "True") + self.assertEqual(self.conf.get('dashboard', 'dashboard_url'), + "http://172.16.52.151/dashboard/") + self.assertEqual(self.conf.get('dashboard', 'login_url'), + "http://172.16.52.151/dashboard/auth/login/") + + def test_configure_horizon_ipv6(self): + mock_function = mock.Mock(return_value=True) + self.useFixture(MonkeyPatch('urllib2.urlopen', mock_function)) + self.conf.set('identity', 'uri', 'http://[::1]:5000/v3', priority=True) + horizon.configure_horizon(self.conf) + self.assertEqual(self.conf.get('service_available', 'horizon'), "True") + self.assertEqual(self.conf.get('dashboard', 'dashboard_url'), + "http://[::1]/dashboard/") + self.assertEqual(self.conf.get('dashboard', 'login_url'), + "http://[::1]/dashboard/auth/login/") diff --git a/config_tempest/tests/services/test_identity.py b/config_tempest/tests/services/test_identity.py new file mode 100644 index 00000000..625c7520 --- /dev/null +++ b/config_tempest/tests/services/test_identity.py @@ -0,0 +1,90 @@ +# Copyright 2018 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from fixtures import MonkeyPatch +import mock + +from config_tempest.services.identity import IdentityService +from config_tempest.tempest_conf import TempestConf +from config_tempest.tests.base import BaseServiceTest + + +class TestIdentityService(BaseServiceTest): + def setUp(self): + super(TestIdentityService, self).setUp() + self.Service = IdentityService("ServiceName", + self.FAKE_URL + 'v2.0/', + self.FAKE_TOKEN, + disable_ssl_validation=False) + + def test_set_extensions(self): + expected_resp = ['OS-DCF', 'NMN'] + self._set_get_extensions(self.Service, expected_resp, + self.FAKE_IDENTITY_EXTENSIONS) + # try with URL containing v3 version + self.Service.service_url = self.FAKE_URL + 'v3' + expected_resp = [] + self._set_get_extensions(self.Service, expected_resp, + self.FAKE_IDENTITY_EXTENSIONS) + + def test_get_extensions(self): + exp_resp = ['OS-INHERIT', 'OS-OAUTH1', + 'OS-SIMPLE-CERT', 'OS-EP-FILTER'] + self.Service.extensions = exp_resp[:2] + self.Service.extensions_v3 = exp_resp[2:] + self.assertItemsEqual(self.Service.get_extensions(), exp_resp) + + def test_set_identity_v3_extensions(self): + expected_resp = ['OS-INHERIT', 'OS-OAUTH1', + 'OS-SIMPLE-CERT', 'OS-EP-FILTER'] + fake_resp = self.FakeRequestResponse() + mocked_requests = mock.Mock() + mocked_requests.return_value = fake_resp + self.useFixture(MonkeyPatch('requests.get', mocked_requests)) + self.Service.service_url = self.FAKE_URL + "v3" + self.Service.set_identity_v3_extensions() + self.assertItemsEqual(self.Service.extensions_v3, expected_resp) + self.assertItemsEqual(self.Service.get_extensions(), expected_resp) + + def test_set_get_versions(self): + exp_resp = ['v3.8'] + self._set_get_versions(self.Service, exp_resp, + self.FAKE_IDENTITY_VERSIONS) + + def test_deserialize_versions(self): + expected_resp = ['v3.8'] + self._test_deserialize_versions(self.Service, + expected_resp, + self.FAKE_IDENTITY_VERSIONS) + self._test_deserialize_versions(self.Service, + expected_resp, + self.FAKE_IDENTITY_VERSION) + expected_resp = ['v2.1', 'v3.8'] + # add not deprecated v2 version to FAKE_IDENTITY_VERSIONS + v2 = {'status': 'stable', 'id': 'v2.1'} + self.FAKE_IDENTITY_VERSIONS['versions']['values'].append(v2) + self._test_deserialize_versions(self.Service, + expected_resp, + self.FAKE_IDENTITY_VERSIONS) + + @mock.patch('config_tempest.services.identity' + '.IdentityService.get_versions') + def test_configure_keystone_feature_flags(self, mock_get_versions): + conf = TempestConf() + mock_get_versions.return_value = ['v3.8', 'v2.0'] + self.Service.configure_keystone_feature_flags(conf) + self.assertEqual( + conf.get('identity-feature-enabled', + 'forbid_global_implied_dsr'), 'True') diff --git a/config_tempest/tests/test_config_tempest_image.py b/config_tempest/tests/services/test_image.py similarity index 59% rename from config_tempest/tests/test_config_tempest_image.py rename to config_tempest/tests/services/test_image.py index b473acc7..ca40496c 100644 --- a/config_tempest/tests/test_config_tempest_image.py +++ b/config_tempest/tests/services/test_image.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- - -# Copyright 2017 Red Hat, Inc. +# Copyright 2018 Red Hat, Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -18,47 +16,86 @@ from fixtures import MonkeyPatch import mock -from config_tempest import main as tool -from config_tempest.tests.base import BaseConfigTempestTest +from config_tempest.services.image import ImageService +from config_tempest.tempest_conf import TempestConf +from config_tempest.tests.base import BaseServiceTest -class TestCreateTempestImages(BaseConfigTempestTest): +class TestImageService(BaseServiceTest): + + CLIENT_MOCK = 'tempest.lib.services.image.v2.images_client.ImagesClient' + IMAGES_LIST = [ + {"status": "active", "name": "ImageName"}, + {"status": "default", "name": "MyImage"}, + {"status": "active", "name": "MyImage"} + ] def setUp(self): - super(TestCreateTempestImages, self).setUp() - self.conf = self._get_conf("v2.0", "v3") - self.client = self._get_clients(self.conf).images - self.image_path = "my_path/my_image.qcow2" - self.allow_creation = False - self.disk_format = ".format" + super(TestImageService, self).setUp() + self.Service = ImageService("ServiceName", + self.FAKE_URL, + self.FAKE_TOKEN, + disable_ssl_validation=False) + self.Service.allow_creation = False + self.Service.image_path = "my_path/my_image.qcow2" + self.Service.disk_format = ".format" + self.Service.client = self.FakeServiceClient() + self.dir = "/img/" + self.conf = TempestConf() self.conf.set("scenario", "img_dir", self.dir) - @mock.patch('config_tempest.main.find_or_upload_image') - def test_create_tempest_images_exception(self, mock_find_upload): - mock_find_upload.side_effect = Exception - exc = Exception - self.assertRaises(exc, - tool.create_tempest_images, - client=self.client, - conf=self.conf, - image_path=self.image_path, - allow_creation=self.allow_creation, - disk_format=self.disk_format) - - @mock.patch('config_tempest.main.find_or_upload_image') + @mock.patch('config_tempest.services.image.ImageService' + '.find_or_upload_image') def _test_create_tempest_images(self, mock_find_upload): mock_find_upload.side_effect = ["id_c", "id_d"] - tool.create_tempest_images(client=self.client, - conf=self.conf, - image_path=self.image_path, - allow_creation=self.allow_creation, - disk_format=self.disk_format) + self.Service.create_tempest_images(conf=self.conf) self.assertEqual(self.conf.get('compute', 'image_ref'), 'id_c') self.assertEqual(self.conf.get('compute', 'image_ref_alt'), 'id_d') self.assertEqual(self.conf.get('scenario', 'img_file'), 'my_image.qcow2') + @mock.patch('config_tempest.services.image.ImageService._find_image') + @mock.patch('config_tempest.services.image.ImageService._download_file') + @mock.patch('config_tempest.services.image.ImageService._upload_image') + def _test_find_or_upload_image_not_found_creation_allowed_format( + self, mock_upload_image, + mock_download_file, mock_find_image, format): + mock_find_image.return_value = None + mock_upload_image.return_value = {"id": "my_fake_id"} + image_source = format + "://any_random_url" + image_dest = "my_dest" + image_name = "my_image" + self.Service.allow_creation = True + image_id = self.Service.find_or_upload_image( + image_id=None, image_dest=image_dest, + image_name=image_name, image_source=image_source) + mock_download_file.assert_called_with(image_source, image_dest) + mock_upload_image.assert_called_with(image_name, image_dest) + self.assertEqual(image_id, "my_fake_id") + + def _mock_list_images(self, return_value, image_name, expected_resp): + + mock_function = mock.Mock(return_value=return_value) + func2mock = self.FAKE_CLIENT_MOCK + '.list_images' + self.useFixture(MonkeyPatch(func2mock, mock_function)) + resp = self.Service._find_image(image_id=None, + image_name=image_name) + self.assertEqual(resp, expected_resp) + + def test_set_get_versions(self): + exp_resp = ['v2.0', 'v2.1'] + self._set_get_versions(self.Service, exp_resp, self.FAKE_VERSIONS) + + @mock.patch('config_tempest.services.image.ImageService' + '.find_or_upload_image') + def test_create_tempest_images_exception(self, mock_find_upload): + mock_find_upload.side_effect = Exception + exc = Exception + self.assertRaises(exc, + self.Service.create_tempest_images, + conf=self.conf) + def test_create_tempest_images_ref_alt_ref(self): self.conf.set('compute', 'image_ref', 'id_a') self.conf.set('compute', 'image_ref_alt', 'id_b') @@ -75,29 +112,68 @@ class TestCreateTempestImages(BaseConfigTempestTest): def test_create_tempest_images_no_ref_no_alt_ref(self): self._test_create_tempest_images() + @mock.patch('config_tempest.services.image.ImageService._find_image') + def test_find_or_upload_image_not_found_creation_not_allowed( + self, mock_find_image): + mock_find_image.return_value = None + exc = Exception + self.assertRaises(exc, self.Service.find_or_upload_image, + image_id=None, image_name=None) -class TestFindImage(BaseConfigTempestTest): + def test_find_or_upload_image_not_found_creation_allowed_http(self): + self._test_find_or_upload_image_not_found_creation_allowed_format( + format="http") - CLIENT_MOCK = 'tempest.lib.services.image.v2.images_client.ImagesClient' - IMAGES_LIST = [ - {"status": "active", "name": "ImageName"}, - {"status": "default", "name": "MyImage"}, - {"status": "active", "name": "MyImage"} - ] + def test_find_or_upload_image_not_found_creation_allowed_https(self): + self._test_find_or_upload_image_not_found_creation_allowed_format( + format="https") - def setUp(self): - super(TestFindImage, self).setUp() - conf = self._get_conf("v2.0", "v3") - self.client = self._get_clients(conf).images + @mock.patch('shutil.copyfile') + @mock.patch('config_tempest.services.image.ImageService._find_image') + @mock.patch('config_tempest.services.image.ImageService._download_file') + @mock.patch('config_tempest.services.image.ImageService._upload_image') + def test_find_or_upload_image_not_found_creation_allowed_ftp_old( + self, mock_upload_image, mock_download_file, mock_find_image, + mock_copy): + mock_find_image.return_value = None + mock_upload_image.return_value = {"id": "my_fake_id"} + # image source does not start with http or https + image_source = "ftp://any_random_url" + image_dest = "place_on_disk" + image_name = "my_image" + self.Service.allow_creation = True + image_id = self.Service.find_or_upload_image( + image_id=None, image_name=image_name, + image_source=image_source, image_dest=image_dest) + mock_copy.assert_called_with(image_source, image_dest) + mock_upload_image.assert_called_with( + image_name, image_dest) + self.assertEqual(image_id, "my_fake_id") - def _mock_list_images(self, return_value, image_name, expected_resp): - mock_function = mock.Mock(return_value=return_value) - func2mock = self.CLIENT_MOCK + '.list_images' - self.useFixture(MonkeyPatch(func2mock, mock_function)) - resp = tool._find_image(client=self.client, - image_id=None, - image_name=image_name) - self.assertEqual(resp, expected_resp) + @mock.patch('os.path.isfile') + @mock.patch('config_tempest.services.image.ImageService._find_image') + def test_find_or_upload_image_found_downloaded( + self, mock_find_image, mock_isfile): + mock_find_image.return_value = \ + {"status": "active", "name": "ImageName", "id": "my_fake_id"} + mock_isfile.return_value = True + image_id = self.Service.find_or_upload_image( + image_id=None, image_name=None) + self.assertEqual(image_id, "my_fake_id") + + @mock.patch('config_tempest.services.image.ImageService._download_image') + @mock.patch('os.path.isfile') + @mock.patch('config_tempest.services.image.ImageService._find_image') + def test_find_or_upload_image_found_not_downloaded( + self, mock_find_image, mock_isfile, mock_download_image): + image_id = "my_fake_id" + mock_find_image.return_value = \ + {"status": "active", "name": "ImageName", "id": image_id} + mock_isfile.return_value = False + image_id = self.Service.find_or_upload_image( + image_id=None, image_name=None) + mock_download_image.assert_called() + self.assertEqual(image_id, "my_fake_id") def test_find_image_found(self): expected_resp = {"status": "default", "name": "MyImage"} @@ -113,106 +189,8 @@ class TestFindImage(BaseConfigTempestTest): def test_find_image_by_id(self): expected_resp = {"id": "001", "status": "active", "name": "ImageName"} mock_function = mock.Mock(return_value=expected_resp) - func2mock = self.CLIENT_MOCK + '.show_image' + func2mock = self.FAKE_CLIENT_MOCK + '.show_image' self.useFixture(MonkeyPatch(func2mock, mock_function)) - resp = tool._find_image(client=self.client, - image_id="001", - image_name="cirros") + resp = self.Service._find_image(image_id="001", + image_name="cirros") self.assertEqual(resp, expected_resp) - - -class TestFindOrUploadImage(BaseConfigTempestTest): - - def setUp(self): - super(TestFindOrUploadImage, self).setUp() - conf = self._get_conf("v2.0", "v3") - self.client = self._get_clients(conf).images - - @mock.patch('config_tempest.main._find_image') - def test_find_or_upload_image_not_found_creation_not_allowed( - self, mock_find_image): - mock_find_image.return_value = None - exc = Exception - self.assertRaises(exc, tool.find_or_upload_image, client=self.client, - image_id=None, image_name=None, - allow_creation=False) - - @mock.patch('config_tempest.main._find_image') - @mock.patch('config_tempest.main._download_file') - @mock.patch('config_tempest.main._upload_image') - def _test_find_or_upload_image_not_found_creation_allowed_format( - self, mock_upload_image, - mock_download_file, mock_find_image, format): - mock_find_image.return_value = None - mock_upload_image.return_value = {"id": "my_fake_id"} - image_source = format + "://any_random_url" - image_dest = "my_dest" - image_name = "my_image" - disk_format = "my_format" - image_id = tool.find_or_upload_image( - client=self.client, image_id=None, image_dest=image_dest, - image_name=image_name, image_source=image_source, - allow_creation=True, disk_format=disk_format) - mock_download_file.assert_called_with(image_source, image_dest) - mock_upload_image.assert_called_with(self.client, - image_name, image_dest, - disk_format) - self.assertEqual(image_id, "my_fake_id") - - def test_find_or_upload_image_not_found_creation_allowed_http(self): - self._test_find_or_upload_image_not_found_creation_allowed_format( - format="http") - - def test_find_or_upload_image_not_found_creation_allowed_https(self): - self._test_find_or_upload_image_not_found_creation_allowed_format( - format="https") - - @mock.patch('shutil.copyfile') - @mock.patch('config_tempest.main._find_image') - @mock.patch('config_tempest.main._download_file') - @mock.patch('config_tempest.main._upload_image') - def test_find_or_upload_image_not_found_creation_allowed_ftp_old( - self, mock_upload_image, mock_download_file, mock_find_image, - mock_copy): - mock_find_image.return_value = None - mock_upload_image.return_value = {"id": "my_fake_id"} - # image source does not start with http or https - image_source = "ftp://any_random_url" - image_dest = "place_on_disk" - disk_format = "my_format" - image_name = "my_image" - image_id = tool.find_or_upload_image( - client=self.client, image_id=None, image_name=image_name, - image_source=image_source, image_dest=image_dest, - allow_creation=True, disk_format=disk_format) - mock_copy.assert_called_with(image_source, image_dest) - mock_upload_image.assert_called_with( - self.client, image_name, image_dest, disk_format) - self.assertEqual(image_id, "my_fake_id") - - @mock.patch('os.path.isfile') - @mock.patch('config_tempest.main._find_image') - def test_find_or_upload_image_found_downloaded( - self, mock_find_image, mock_isfile): - mock_find_image.return_value = \ - {"status": "active", "name": "ImageName", "id": "my_fake_id"} - mock_isfile.return_value = True - image_id = tool.find_or_upload_image( - client=self.client, image_id=None, - image_name=None, allow_creation=True) - self.assertEqual(image_id, "my_fake_id") - - @mock.patch('config_tempest.main._download_image') - @mock.patch('os.path.isfile') - @mock.patch('config_tempest.main._find_image') - def test_find_or_upload_image_found_not_downloaded( - self, mock_find_image, mock_isfile, mock_download_image): - image_id = "my_fake_id" - mock_find_image.return_value = \ - {"status": "active", "name": "ImageName", "id": image_id} - mock_isfile.return_value = False - image_id = tool.find_or_upload_image( - client=self.client, image_id=None, - image_name=None, allow_creation=True) - mock_download_image.assert_called() - self.assertEqual(image_id, "my_fake_id") diff --git a/config_tempest/tests/services/test_network.py b/config_tempest/tests/services/test_network.py new file mode 100644 index 00000000..642a43a8 --- /dev/null +++ b/config_tempest/tests/services/test_network.py @@ -0,0 +1,101 @@ +# Copyright 2018 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import mock + +from config_tempest.services.network import NetworkService +from config_tempest.tempest_conf import TempestConf +from config_tempest.tests.base import BaseServiceTest + + +class TestNetworkService(BaseServiceTest): + + FAKE_NETWORK_LIST = { + 'networks': [{ + 'provider:physical_network': None, + 'id': '1ea533d7-4c65-4f25', + 'router:external': True, + 'availability_zone_hints': [], + 'availability_zones': [], + 'ipv4_address_scope': None, + 'status': 'ACTIVE', + 'subnets': ['fake_subnet'], + 'label': 'my_fake_label', + 'name': 'tempest-network', + 'admin_state_up': True, + }] + } + + def setUp(self): + super(TestNetworkService, self).setUp() + self.conf = TempestConf() + self.Service = NetworkService("ServiceName", + self.FAKE_URL, + self.FAKE_TOKEN, + disable_ssl_validation=False) + self.Service.client = self.FakeServiceClient() + + def test_set_get_extensions(self): + exp_resp = ['NMN', 'OS-DCF'] + self._set_get_extensions(self.Service, exp_resp, self.FAKE_EXTENSIONS) + + def test_tempest_network_id_not_found(self): + return_mock = mock.Mock(return_value={"networks": []}) + self.Service.client.list_networks = return_mock + self.Service._public_network_id = "doesn't_exist" + self.assertRaises(ValueError, + self.Service._supplied_network) + + def test_create_network_id_supplied_by_user(self): + return_mock = mock.Mock(return_value=self.FAKE_NETWORK_LIST) + self.Service.client.list_networks = return_mock + self.Service._public_network_id = '1ea533d7-4c65-4f25' + self.Service._supplied_network() + self.assertEqual(self.Service._public_network_name, 'tempest-network') + + def test_create_network_auto_discover(self): + return_mock = mock.Mock(return_value=self.FAKE_NETWORK_LIST) + self.Service.client.list_networks = return_mock + self.Service._discover_network() + self.assertEqual(self.Service._public_network_id, '1ea533d7-4c65-4f25') + self.assertEqual(self.Service._public_network_name, 'tempest-network') + + @mock.patch('config_tempest.services.network.LOG') + def test_create_network_auto_discover_not_found(self, mock_logging): + # delete subnets => network will not be found + self.FAKE_NETWORK_LIST['networks'][0]['subnets'] = [] + return_mock = mock.Mock(return_value=self.FAKE_NETWORK_LIST) + self.Service.client.list_networks = return_mock + self.Service._discover_network() + # check if LOG.error was called + self.assertTrue(mock_logging.error.called) + + def test_network_not_discovered(self): + FAKE_NETWORK_LIST = { + 'networks': [{ + 'label': "" + }] + } + exception = Exception + self.Service.client.return_value = FAKE_NETWORK_LIST + self.assertRaises(exception, + self.Service.create_tempest_networks_nova, + conf=self.conf) + + def test_create_fixed_network(self): + self.Service.client.return_value = self.FAKE_NETWORK_LIST + self.Service.create_tempest_networks_nova(conf=self.conf) + self.assertEqual(self.conf.get('compute', 'fixed_network_name'), + 'my_fake_label') diff --git a/config_tempest/tests/services/test_object_storage.py b/config_tempest/tests/services/test_object_storage.py new file mode 100644 index 00000000..19b6825e --- /dev/null +++ b/config_tempest/tests/services/test_object_storage.py @@ -0,0 +1,37 @@ +# Copyright 2018 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from config_tempest.services.object_storage import ObjectStorageService +from config_tempest.tests.base import BaseServiceTest + + +class TestObjectStorageService(BaseServiceTest): + def setUp(self): + super(TestObjectStorageService, self).setUp() + self.Service = ObjectStorageService("ServiceName", + self.FAKE_URL, + self.FAKE_TOKEN, + disable_ssl_validation=False) + + def test_set_get_extensions(self): + expected_resp = ['formpost', 'ratelimit', + 'methods', 'account_quotas'] + self._fake_service_do_get_method(self.FAKE_STORAGE_EXTENSIONS) + self.Service.set_extensions(object_store_discovery=True) + self.assertItemsEqual(self.Service.extensions, expected_resp) + self.assertItemsEqual(self.Service.get_extensions(), expected_resp) + self.Service.set_extensions() + self.assertItemsEqual(self.Service.extensions, []) + self.assertItemsEqual(self.Service.get_extensions(), []) diff --git a/config_tempest/tests/services/test_services.py b/config_tempest/tests/services/test_services.py new file mode 100644 index 00000000..e441fdd8 --- /dev/null +++ b/config_tempest/tests/services/test_services.py @@ -0,0 +1,120 @@ +# Copyright 2018 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from config_tempest.services.services import Services +from config_tempest.tests.base import BaseConfigTempestTest +import mock + + +class TestServices(BaseConfigTempestTest): + + FAKE_ENTRY = { + 'endpoints': [ + { + 'region': 'my_region', + 'interface': 'public' + }, + { + 'region': 'other_region', + 'interface': 'private' + } + ] + } + + def setUp(self): + super(TestServices, self).setUp() + + @mock.patch('config_tempest.services.services.Services.discover') + def _create_services_instance(self, mock_discover): + conf = self._get_conf('v2', 'v3') + creds = self._get_creds(conf) + clients = mock.Mock() + services = Services(clients, conf, creds) + return services + + def test_get_endpoints_api_2(self): + services = self._create_services_instance() + services._region = 'my_region' + resp = services.get_endpoints(self.FAKE_ENTRY) + self.assertEqual(resp, self.FAKE_ENTRY['endpoints'][0]) + services._region = 'doesnt_exist_region' + resp = services.get_endpoints(self.FAKE_ENTRY) + self.assertEqual(resp, self.FAKE_ENTRY['endpoints'][0]) + services._region = 'other_region' + resp = services.get_endpoints(self.FAKE_ENTRY) + self.assertEqual(resp, self.FAKE_ENTRY['endpoints'][1]) + + def test_get_endpoints_api_3(self): + services = self._create_services_instance() + services._creds.api_version = 3 + services._region = 'my_region' + resp = services.get_endpoints(self.FAKE_ENTRY) + self.assertEqual(resp, self.FAKE_ENTRY['endpoints'][0]) + services._region = 'other_region' + resp = services.get_endpoints(self.FAKE_ENTRY) + self.assertEqual(resp, self.FAKE_ENTRY['endpoints'][0]) + + def test_get_endpoints_no_endpoints(self): + services = self._create_services_instance() + resp = services.get_endpoints({'endpoints': []}) + self.assertEqual(resp, []) + + def test_set_catalog_and_url(self): + services = self._create_services_instance() + # api version = 2 + services.set_catalog_and_url() + self.assertEqual(services.service_catalog, 'serviceCatalog') + self.assertEqual(services.public_url, 'publicURL') + # api version = 3 + services._creds.api_version = 3 + services.set_catalog_and_url() + self.assertEqual(services.service_catalog, 'catalog') + self.assertEqual(services.public_url, 'url') + + def test_edit_identity_url(self): + services = self._create_services_instance() + url_port = 'https://10.0.0.101:13000/v2.0' + identity_url = 'https://10.0.0.101/identity' + url_no_port = 'https://10.0.0.101/v2.0' + services._clients.auth_provider.auth_url = url_port + url = services.edit_identity_url(url_port) + self.assertEqual(url_port, url) + url = services.edit_identity_url(identity_url) + self.assertEqual("https://10.0.0.101:13000/identity/v2", url) + url = services.edit_identity_url(url_no_port) + self.assertEqual(url_no_port, url) + services._clients.auth_provider.auth_url = url_no_port + url = services.edit_identity_url(identity_url) + self.assertEqual(identity_url + "/v2", url) + + def test_get_service(self): + services = self._create_services_instance() + exp_resp = mock.Mock() + exp_resp.name = 'my_service' + services._services = [exp_resp] + resp = services.get_service('my_service') + self.assertEqual(resp, exp_resp) + resp = services.get_service('my') + self.assertEqual(resp, None) + + def test_is_service(self): + services = self._create_services_instance() + service = mock.Mock() + service.name = 'my_service' + services._services = [service] + resp = services.is_service('my_service') + self.assertEqual(resp, True) + resp = services.is_service('other_service') + self.assertEqual(resp, False) diff --git a/config_tempest/tests/services/test_volume.py b/config_tempest/tests/services/test_volume.py new file mode 100644 index 00000000..dce961f0 --- /dev/null +++ b/config_tempest/tests/services/test_volume.py @@ -0,0 +1,65 @@ +# Copyright 2018 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import mock + +from config_tempest.services import volume +from config_tempest.tempest_conf import TempestConf +from config_tempest.tests.base import BaseServiceTest + + +class TestVolumeService(BaseServiceTest): + def setUp(self): + super(TestVolumeService, self).setUp() + self.Service = volume.VolumeService("ServiceName", + self.FAKE_URL, + self.FAKE_TOKEN, + disable_ssl_validation=False) + self.conf = TempestConf() + + def test_set_get_extensions(self): + exp_resp = ['NMN', 'OS-DCF'] + self._set_get_extensions(self.Service, exp_resp, self.FAKE_EXTENSIONS) + + def test_set_get_versions(self): + exp_resp = ['v2.0', 'v2.1'] + self._set_get_versions(self.Service, exp_resp, self.FAKE_VERSIONS) + + @mock.patch('config_tempest.services.volume.C.LOG') + def test_check_volume_backup_service_no_volume(self, mock_logging): + volume.check_volume_backup_service(self.conf, None, False) + self.assertTrue(mock_logging.info.called) + + def test_check_volume_backup_service_state_down(self): + client_service_mock = self.FakeServiceClient(services={ + 'services': [ + { + "state": "down" + } + ] + }) + volume.check_volume_backup_service(self.conf, + client_service_mock, True) + self.assertEqual(self.conf.get('volume-feature-enabled', + 'backup'), 'False') + + def test_check_volume_backup_service_no_service(self): + client_service_mock = self.FakeServiceClient(services={ + 'services': [] + }) + volume.check_volume_backup_service(self.conf, + client_service_mock, True) + self.assertEqual(self.conf.get('volume-feature-enabled', + 'backup'), 'False') diff --git a/config_tempest/tests/test_api_discovery_methods.py b/config_tempest/tests/test_api_discovery_methods.py deleted file mode 100644 index 8ab4813b..00000000 --- a/config_tempest/tests/test_api_discovery_methods.py +++ /dev/null @@ -1,128 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright 2017 Red Hat, Inc. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from config_tempest import api_discovery as api -from config_tempest.tests.base import BaseServiceTest -from fixtures import MonkeyPatch -from mock import Mock - - -class TestApiDiscoveryMethods(BaseServiceTest): - - FAKE_AUTH_DATA = ( - { - 'serviceCatalog': [ - { - 'endpoints_links': [], - 'endpoints': [{ - 'adminURL': 'http://172.16.52.151:8080/v1/AUTH_402', - 'region': 'RegionOne', - 'publicURL': 'http://172.16.52.151:8080/v1/AUTH_402', - 'internalURL': 'http://172.16.52.151:8080/v1/AUTH_402', - 'id': '22c221db6ffd4236a3fd054c60aa8fd6' - }], - 'type': 'object-store', - 'name': 'swift' - } - ] - } - ) - - EXPECTED_RESP = ( - { - 'object-store': - { - 'extensions': 'discovered extensions', - 'url': 'http://172.16.52.151:8080/v1/AUTH_402', - 'versions': [] - } - } - ) - - FAKE_AUTH_DATA_V3 = ( - { - 'serviceCatalog': [ - { - 'endpoints_links': [], - 'endpoints': [{ - 'adminURL': 'http://172.16.52.151:8774/v3/402', - 'region': 'RegionOne', - 'publicURL': 'http://172.16.52.151:8774/v3/402', - 'internalURL': 'http://172.16.52.151:8774/v3/402', - 'id': '01bbde4f9fb54d35badf0561a53b2bdb' - }], - 'type': 'compute', - 'name': 'nova' - } - ] - } - ) - - EXPECTED_RESP_V3 = ( - { - 'compute': - { - 'url': 'http://172.16.52.151:8774/v3/402', - 'versions': 'discovered versions' - } - } - ) - - def setUp(self): - super(TestApiDiscoveryMethods, self).setUp() - - class FakeAuthProvider(object): - def __init__(self, auth_url, auth_data): - self.auth_url = auth_url # 'http://172.16.52.151:5000/v2.0' - self.auth_data = auth_data - - def get_auth(self): - token = 'AAAAABYkehvCCJOsO2GWGqBbxk0mhH7VulICOW' - return (token, self.auth_data) - - def _test_discover(self, url, data, function2mock, - mock_ret_val, expected_resp): - provider = self.FakeAuthProvider(url, data) - mocked_function = Mock() - mocked_function.return_value = mock_ret_val - self.useFixture(MonkeyPatch(function2mock, mocked_function)) - resp = api.discover(provider, "RegionOne") - self.assertEqual(resp, expected_resp) - - def test_get_identity_v3_extensions(self): - expected_resp = ['OS-INHERIT', 'OS-OAUTH1', - 'OS-SIMPLE-CERT', 'OS-EP-FILTER'] - fake_resp = self.FakeRequestResponse() - mocked_requests = Mock() - mocked_requests.return_value = fake_resp - self.useFixture(MonkeyPatch('requests.get', mocked_requests)) - resp = api.get_identity_v3_extensions(self.FAKE_URL + "v3") - self.assertItemsEqual(resp, expected_resp) - - def test_discover_with_v2_url(self): - function2mock = ('config_tempest.api_discovery.' - 'ObjectStorageService.get_extensions') - mock_ret_val = "discovered extensions" - self._test_discover(self.FAKE_URL + "v2.0", self.FAKE_AUTH_DATA, - function2mock, mock_ret_val, self.EXPECTED_RESP) - - def test_discover_with_v3_url(self): - function2mock = ('config_tempest.api_discovery.' - 'ComputeService.get_versions') - mock_ret_val = "discovered versions" - self._test_discover(self.FAKE_URL + "v3", self.FAKE_AUTH_DATA_V3, - function2mock, mock_ret_val, self.EXPECTED_RESP_V3) diff --git a/config_tempest/tests/test_api_discovery_services.py b/config_tempest/tests/test_api_discovery_services.py deleted file mode 100644 index 9768df5b..00000000 --- a/config_tempest/tests/test_api_discovery_services.py +++ /dev/null @@ -1,173 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright 2017 Red Hat, Inc. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from config_tempest import api_discovery as api -from config_tempest.tests.base import BaseServiceTest -import mock -import unittest - - -class TestService(BaseServiceTest): - def setUp(self): - super(TestService, self).setUp() - self.Service = api.Service("ServiceName", - self.FAKE_URL, - self.FAKE_TOKEN, - disable_ssl_validation=False) - - def _mocked_do_get(self, mock_urllib3): - mock_http = mock_urllib3.PoolManager() - expected_resp = mock_http.request('GET', - self.FAKE_URL, - self.FAKE_HEADERS) - return expected_resp.data - - @unittest.skip('Failing due to Storyboard: 2001245') - @mock.patch('config_tempest.api_discovery.urllib3') - def test_do_get(self, mock_urllib3): - resp = self.Service.do_get(self.FAKE_URL) - expected_resp = self._mocked_do_get(mock_urllib3) - self.assertEqual(resp, expected_resp) - - def test_service_headers(self): - self.assertEqual(self.Service.headers, self.FAKE_HEADERS) - - -class TestVersionedService(BaseServiceTest): - def setUp(self): - super(TestVersionedService, self).setUp() - self.Service = api.VersionedService("ServiceName", - self.FAKE_URL, - self.FAKE_TOKEN, - disable_ssl_validation=False) - - def test_get_versions(self): - expected_resp = ['v2.0', 'v2.1'] - self._fake_service_do_get_method(self.FAKE_VERSIONS) - resp = self.Service.get_versions() - self.assertItemsEqual(resp, expected_resp) - - def test_deserialize_versions(self): - expected_resp = ['v2.0', 'v2.1'] - self._test_deserialize_versions(self.Service, - expected_resp, - self.FAKE_VERSIONS) - - -class TestComputeService(BaseServiceTest): - def setUp(self): - super(TestComputeService, self).setUp() - self.Service = api.ComputeService("ServiceName", - self.FAKE_URL, - self.FAKE_TOKEN, - disable_ssl_validation=False) - - def test_get_extensions(self): - expected_resp = ['NMN', 'OS-DCF'] - self._get_extensions(self.Service, expected_resp, self.FAKE_EXTENSIONS) - - def test_get_service_class(self): - self._test_get_service_class('compute', api.ComputeService) - - -class TestImageService(BaseServiceTest): - def setUp(self): - super(TestImageService, self).setUp() - - def test_get_service_class(self): - self._test_get_service_class('image', api.ImageService) - - -class TestNetworkService(BaseServiceTest): - def setUp(self): - super(TestNetworkService, self).setUp() - self.Service = api.NetworkService("ServiceName", - self.FAKE_URL, - self.FAKE_TOKEN, - disable_ssl_validation=False) - - def test_get_extensions(self): - expected_resp = ['NMN', 'OS-DCF'] - self._get_extensions(self.Service, expected_resp, self.FAKE_EXTENSIONS) - - def test_get_service_class(self): - self._test_get_service_class('network', api.NetworkService) - - -class TestVolumeService(BaseServiceTest): - def setUp(self): - super(TestVolumeService, self).setUp() - self.Service = api.VolumeService("ServiceName", - self.FAKE_URL, - self.FAKE_TOKEN, - disable_ssl_validation=False) - - def test_get_extensions(self): - expected_resp = ['NMN', 'OS-DCF'] - self._get_extensions(self.Service, expected_resp, self.FAKE_EXTENSIONS) - - def test_get_service_class(self): - self._test_get_service_class('volumev3', api.VolumeService) - - -class TestIdentityService(BaseServiceTest): - def setUp(self): - super(TestIdentityService, self).setUp() - self.Service = api.IdentityService("ServiceName", - self.FAKE_URL + 'v2.0/', - self.FAKE_TOKEN, - disable_ssl_validation=False) - - def test_get_extensions(self): - expected_resp = ['OS-DCF', 'NMN'] - self._get_extensions(self.Service, expected_resp, - self.FAKE_IDENTITY_EXTENSIONS) - - def test_deserialize_versions(self): - expected_resp = ['v3.8'] - self._test_deserialize_versions(self.Service, - expected_resp, - self.FAKE_IDENTITY_VERSIONS) - expected_resp = ['v2.1', 'v3.8'] - # add not deprecated v2 version to FAKE_IDENTITY_VERSIONS - v2 = {'status': 'stable', 'id': 'v2.1'} - self.FAKE_IDENTITY_VERSIONS['versions']['values'].append(v2) - self._test_deserialize_versions(self.Service, - expected_resp, - self.FAKE_IDENTITY_VERSIONS) - - def test_get_service_class(self): - self._test_get_service_class('identity', api.IdentityService) - - -class TestObjectStorageService(BaseServiceTest): - def setUp(self): - super(TestObjectStorageService, self).setUp() - self.Service = api.ObjectStorageService("ServiceName", - self.FAKE_URL, - self.FAKE_TOKEN, - disable_ssl_validation=False) - - def test_get_extensions(self): - expected_resp = ['formpost', 'ratelimit', - 'methods', 'account_quotas'] - self._get_extensions(self.Service, expected_resp, - self.FAKE_STORAGE_EXTENSIONS) - - def test_get_service_class(self): - self._test_get_service_class('object-store', - api.ObjectStorageService) diff --git a/config_tempest/tests/test_clients.py b/config_tempest/tests/test_clients.py index 7d747931..c13a6857 100644 --- a/config_tempest/tests/test_clients.py +++ b/config_tempest/tests/test_clients.py @@ -16,9 +16,86 @@ # under the License. from fixtures import MonkeyPatch +import logging import mock +from config_tempest.clients import ClientManager +from config_tempest.clients import ProjectsClient from config_tempest.tests.base import BaseConfigTempestTest +from tempest.lib import exceptions + +# disable logging when running unit tests +logging.disable(logging.CRITICAL) + + +class TestProjectsClient(BaseConfigTempestTest): + LIST_PROJECTS = [{'name': 'my_name'}] + + def setUp(self): + super(TestProjectsClient, self).setUp() + self.conf = self._get_conf("v2.0", "v3") + self.creds = self._get_creds(self.conf) + # self.projects_manager = ProjectsClient(self.conf, self.creds) + self.client_manager = ClientManager(self.conf, self.creds) + + def _get_projects_client(self, identity_version): + return ProjectsClient( + self.client_manager.auth_provider, + self.conf.get_defaulted('identity', 'catalog_type'), + self.client_manager.identity_region, + 'publicURL', + identity_version, + **self.client_manager._get_default_params(self.conf)) + + def test_init(self): + resp = self._get_projects_client('v2') + self.assertEqual(type(resp.client).__name__, 'TenantsClient') + # use v3 identity version and check if ProjectClient is instantiated + resp = self._get_projects_client('v3') + self.assertEqual(type(resp.client).__name__, 'ProjectsClient') + + @mock.patch('tempest.lib.services.identity.v2.tenants_client.' + 'TenantsClient.list_tenants') + def test_get_project_by_name_v2(self, mock_list_tenant): + client = self._get_projects_client('v2') + mock_list_tenant.return_value = {'tenants': self.LIST_PROJECTS} + resp = client.get_project_by_name('my_name') + self.assertEqual(resp['name'], 'my_name') + + @mock.patch('tempest.lib.services.identity.v3.projects_client.' + 'ProjectsClient.list_projects') + def test_get_project_by_name_v3(self, mock_list_project): + client = self._get_projects_client('v3') + mock_list_project.return_value = {'projects': self.LIST_PROJECTS} + resp = client.get_project_by_name('my_name') + self.assertEqual(resp['name'], 'my_name') + + @mock.patch('tempest.lib.services.identity.v3.projects_client.' + 'ProjectsClient.list_projects') + def test_get_project_by_name_not_found(self, mock_list_project): + client = self._get_projects_client('v3') + mock_list_project.return_value = {'projects': self.LIST_PROJECTS} + try: + client.get_project_by_name('doesnt_exist') + except exceptions.NotFound: + # expected behaviour + pass + + @mock.patch('tempest.lib.services.identity.v2.tenants_client.' + 'TenantsClient.create_tenant') + def test_create_project_v2(self, mock_create_tenant): + client = self._get_projects_client('v2') + client.create_project('name', 'description') + mock_create_tenant.assert_called_with( + name='name', description='description') + + @mock.patch('tempest.lib.services.identity.v3.projects_client.' + 'ProjectsClient.create_project') + def test_create_project_v3(self, mock_create_project): + client = self._get_projects_client('v3') + client.create_project('name', 'description') + mock_create_project.assert_called_with( + name='name', description='description') class TestClientManager(BaseConfigTempestTest): @@ -26,41 +103,49 @@ class TestClientManager(BaseConfigTempestTest): def setUp(self): super(TestClientManager, self).setUp() self.conf = self._get_conf("v2.0", "v3") - self.client = self._get_clients(self.conf) + self.creds = self._get_creds(self.conf) + # self.client = self._get_clients(self.conf) # ? + self.client_manager = ClientManager(self.conf, self.creds) def test_init_manager_as_admin(self): + self.creds = self._get_creds(self.conf, True) mock_function = mock.Mock(return_value={"id": "my_fake_id"}) func2mock = ('config_tempest.clients.ProjectsClient.' 'get_project_by_name') self.useFixture(MonkeyPatch(func2mock, mock_function)) - self._get_clients(self.conf, self._get_creds(self.conf, admin=True)) - # check if admin credentials were set - admin_tenant = self.conf.get("identity", "admin_tenant_name") - admin_password = self.conf.get("identity", "admin_password") - self.assertEqual(self.conf.get("identity", "admin_username"), "admin") - self.assertEqual(admin_tenant, "adminTenant") - self.assertEqual(admin_password, "adminPass") + ClientManager(self.conf, self.creds) # check if admin tenant id was set admin_tenant_id = self.conf.get("identity", "admin_tenant_id") self.assertEqual(admin_tenant_id, "my_fake_id") - def test_init_manager_as_admin_using_new_auth(self): - self.conf = self._get_alt_conf("v2.0", "v3") - self.client = self._get_clients(self.conf) - mock_function = mock.Mock(return_value={"id": "my_fake_id"}) - func2mock = ('config_tempest.clients.ProjectsClient' - '.get_project_by_name') - self.useFixture(MonkeyPatch(func2mock, mock_function)) - self._get_clients(self.conf, self._get_creds(self.conf, admin=True)) - # check if admin credentials were set - admin_tenant = self.conf.get("auth", "admin_project_name") - admin_password = self.conf.get("auth", "admin_password") - self.assertEqual(self.conf.get("auth", "admin_username"), "admin") - self.assertEqual(admin_tenant, "adminTenant") - self.assertEqual(admin_password, "adminPass") - # check if admin tenant id was set - admin_tenant_id = self.conf.get("identity", "admin_tenant_id") - self.assertEqual(admin_tenant_id, "my_fake_id") - use_dynamic_creds_bool = self.conf.get("auth", - "use_dynamic_credentials") - self.assertEqual(use_dynamic_creds_bool, "True") + def test_get_service_client(self): + resp = self.client_manager.get_service_client('image') + self.assertEqual(resp, self.client_manager.images) + resp = self.client_manager.get_service_client('network') + self.assertEqual(resp, self.client_manager) + resp = self.client_manager.get_service_client('doesnt_exist') + self.assertEqual(resp, None) + + def test_set_users_client(self): + self.client_manager.users = None + self.client_manager.set_users_client( + self.client_manager.auth_provider, + self.creds.identity_version, + self.conf.get_defaulted('identity', 'catalog_type'), + 'publicURL', + self.client_manager._get_default_params(self.conf)) + self.assertEqual( + type(self.client_manager.users).__name__, + 'UsersClient') + + def test_set_roles_client(self): + self.client_manager.roles = None + self.client_manager.set_roles_client( + self.client_manager.auth_provider, + self.creds.identity_version, + self.conf.get_defaulted('identity', 'catalog_type'), + 'publicURL', + self.client_manager._get_default_params(self.conf)) + self.assertEqual( + type(self.client_manager.roles).__name__, + 'RolesClient') diff --git a/config_tempest/tests/test_config_tempest.py b/config_tempest/tests/test_config_tempest.py index 0cbd3fdc..accf3a81 100644 --- a/config_tempest/tests/test_config_tempest.py +++ b/config_tempest/tests/test_config_tempest.py @@ -120,250 +120,3 @@ class TestOsClientConfigSupport(BaseConfigTempestTest): self.conf.get('identity', 'admin_username'), self.conf.get('identity', 'admin_password'), self.conf.get('identity', 'admin_tenant_name')) - - -class TestConfigTempest(BaseConfigTempestTest): - - FAKE_SERVICES = { - 'compute': { - 'url': 'http://172.16.52.151:8774/v2.1/402486', - 'extensions': ['NMN', 'OS-DCF', 'OS-EXT-AZ', 'OS-EXT-IMG-SIZE'], - 'versions': ['v2.0', 'v2.1'] - }, - 'network': { - 'url': 'http://172.16.52.151:9696', - 'extensions': ['default-subnetpools', 'network-ip-availability'], - 'versions': ['v2.0'] - }, - 'image': { - 'url': 'http://172.16.52.151:9292', - 'extensions': [], - 'versions': ['v2.4', 'v2.3', 'v2.2'] - }, - 'volumev3': { - 'url': 'http://172.16.52.151:8776/v3/402486', - 'extensions': ['OS-SCH-HNT', 'os-hosts'], - 'versions': ['v2.0', 'v3.0'] - }, - 'volume': { - 'url': 'http://172.16.52.151:8776/v1/402486', - 'extensions': [], - 'versions': [] - }, - 'identity': { - 'url': 'http://172.16.52.151:5000/v3', - 'versions': ['v3.8', 'v2.0'] - }, - 'ec2': { - 'url': 'http://172.16.52.151:5000' - }, - 's3': { - 'url': 'http://172.16.52.151:5000' - } - } - - def setUp(self): - super(TestConfigTempest, self).setUp() - self.conf = self._get_conf("v2.0", "v3") - - def _mock_get_identity_v3_extensions(self): - mock_function = mock.Mock(return_value=['FAKE-EXTENSIONS']) - func2mock = 'config_tempest.api_discovery.get_identity_v3_extensions' - self.useFixture(MonkeyPatch(func2mock, mock_function)) - - def test_check_volume_backup_service(self): - client = self._get_clients(self.conf).volume_service - CLIENT_MOCK = ('tempest.lib.services.volume.v2.' - 'services_client.ServicesClient') - func2mock = '.list_services' - mock_function = mock.Mock(return_value={'services': []}) - self.useFixture(MonkeyPatch(CLIENT_MOCK + func2mock, mock_function)) - tool.check_volume_backup_service(client, self.conf, self.FAKE_SERVICES) - self.assertEqual(self.conf.get('volume-feature-enabled', 'backup'), - 'False') - - def test_check_ceilometer_service(self): - client = self._get_clients(self.conf).service_client - CLIENT_MOCK = ('tempest.lib.services.identity.v3.' - 'services_client.ServicesClient') - func2mock = '.list_services' - mock_function = mock.Mock(return_value={'services': [ - {'name': 'ceilometer', 'enabled': True, 'type': 'metering'}]}) - - self.useFixture(MonkeyPatch(CLIENT_MOCK + func2mock, mock_function)) - tool.check_ceilometer_service(client, self.conf, self.FAKE_SERVICES) - self.assertEqual(self.conf.get('service_available', 'ceilometer'), - 'True') - - def test_configure_keystone_feature_flags(self): - tool.configure_keystone_feature_flags(self.conf, self.FAKE_SERVICES) - self.assertEqual( - self.conf.get('identity-feature-enabled', - 'forbid_global_implied_dsr'), 'True') - - def test_configure_boto(self): - tool.configure_boto(self.conf, self.FAKE_SERVICES) - expected_url = "http://172.16.52.151:5000" - self.assertEqual(self.conf.get("boto", "ec2_url"), expected_url) - self.assertEqual(self.conf.get("boto", "s3_url"), expected_url) - - def test_configure_horizon_ipv4(self): - mock_function = mock.Mock(return_value=True) - self.useFixture(MonkeyPatch('urllib2.urlopen', mock_function)) - tool.configure_horizon(self.conf) - self.assertEqual(self.conf.get('service_available', 'horizon'), "True") - self.assertEqual(self.conf.get('dashboard', 'dashboard_url'), - "http://172.16.52.151/dashboard/") - self.assertEqual(self.conf.get('dashboard', 'login_url'), - "http://172.16.52.151/dashboard/auth/login/") - - def test_configure_horizon_ipv6(self): - mock_function = mock.Mock(return_value=True) - self.useFixture(MonkeyPatch('urllib2.urlopen', mock_function)) - self.conf.set('identity', 'uri', 'http://[::1]:5000/v3', priority=True) - tool.configure_horizon(self.conf) - self.assertEqual(self.conf.get('service_available', 'horizon'), "True") - self.assertEqual(self.conf.get('dashboard', 'dashboard_url'), - "http://[::1]/dashboard/") - self.assertEqual(self.conf.get('dashboard', 'login_url'), - "http://[::1]/dashboard/auth/login/") - - def test_discovered_services(self): - self._mock_get_identity_v3_extensions() - tool.configure_discovered_services(self.conf, self.FAKE_SERVICES) - # check enabled services - enabled_services = ["image", "volume", "compute", "network"] - # iterating through tuples = (service_name, codename) - for service in tool.SERVICE_NAMES.iteritems(): - if service[0] in enabled_services: - enabled = "True" - else: - enabled = "False" - self.assertEqual(self.conf.get("service_available", service[1]), - enabled) - - # check versions - for service, service_info in tool.SERVICE_VERSIONS.iteritems(): - section = service + '-feature-enabled' - for version in service_info['supported_versions']: - # only image v1 is expected to be False - exp_support = str(not(service == "image" and version == "v1")) - self.assertEqual(self.conf.get(section, 'api_' + version), - exp_support) - - # check extensions - for service, ext_key in tool.SERVICE_EXTENSION_KEY.iteritems(): - if service in self.FAKE_SERVICES: - section = service + '-feature-enabled' - if service == "identity": - exp_ext = ",FAKE-EXTENSIONS" - else: - extensions = self.FAKE_SERVICES[service]['extensions'] - exp_ext = ','.join(extensions) - self.assertEqual(self.conf.get(section, 'api_extensions'), - exp_ext) - - def test_discovered_services_volume_service_disabled(self): - self.conf.set("services", "volume", "False") - self._mock_get_identity_v3_extensions() - tool.configure_discovered_services(self.conf, self.FAKE_SERVICES) - self.assertFalse(self.conf.has_option("service_available", "cinder")) - self.assertFalse(self.conf.has_option("volume-feature-enabled", - "api_v1")) - self.assertFalse(self.conf.has_option("volume-feature-enabled", - "api_v2")) - - -class TestFlavors(BaseConfigTempestTest): - """Flavors test class - - Tests for create_tempest_flavors and find_or_create_flavor methods. - """ - - CLIENT_MOCK = 'tempest.lib.services.compute.flavors_client.FlavorsClient' - FLAVORS_LIST = [ - {"id": "Fakeid", "name": "Name"}, - {"id": "MyFakeID", "name": "MyID"} - ] - - def setUp(self): - super(TestFlavors, self).setUp() - self.conf = self._get_conf("v2.0", "v3") - self.client = self._get_clients(self.conf).flavors - - def _mock_create_tempest_flavor(self, mock_function): - func2mock = 'config_tempest.main.find_or_create_flavor' - self.useFixture(MonkeyPatch(func2mock, mock_function)) - tool.create_tempest_flavors(client=self.client, - conf=self.conf, - allow_creation=True) - - def _mock_find_or_create_flavor(self, return_value, func2mock, flavor_name, - expected_resp, allow_creation=False, - flavor_id=None): - mock_function = mock.Mock(return_value=return_value) - self.useFixture(MonkeyPatch(self.CLIENT_MOCK + func2mock, - mock_function)) - resp = tool.find_or_create_flavor(self.client, - flavor_id=flavor_id, - flavor_name=flavor_name, - allow_creation=allow_creation) - self.assertEqual(resp, expected_resp) - - def test_create_tempest_flavors(self): - mock_function = mock.Mock(return_value="FakeID") - self._mock_create_tempest_flavor(mock_function) - self.assertEqual(self.conf.get('compute', 'flavor_ref'), "FakeID") - self.assertEqual(self.conf.get('compute', 'flavor_ref_alt'), "FakeID") - calls = [mock.call(self.client, None, 'm1.nano', True, ram=64), - mock.call(self.client, None, 'm1.micro', True, ram=128)] - mock_function.assert_has_calls(calls, any_order=True) - - def test_create_tempest_flavors_overwrite(self): - mock_function = mock.Mock(return_value="FakeID") - self.conf.set('compute', 'flavor_ref', "FAKE_ID") - self.conf.set('compute', 'flavor_ref_alt', "FAKE_ID") - self._mock_create_tempest_flavor(mock_function) - calls = [mock.call(self.client, "FAKE_ID", 'm1.nano', True, ram=64), - mock.call(self.client, "FAKE_ID", 'm1.micro', True, ram=128)] - mock_function.assert_has_calls(calls, any_order=True) - - def test_create_flavor_not_allowed(self): - # mock list_flavors() to return empty list - mock_function = mock.Mock(return_value={"flavors": []}) - self.useFixture(MonkeyPatch(self.CLIENT_MOCK + '.list_flavors', - mock_function)) - exc = Exception - self.assertRaises(exc, - tool.find_or_create_flavor, - client=self.client, - flavor_id="id", - flavor_name="name", - allow_creation=False) - - def test_create_flavor(self): - return_value = {"flavor": {"id": "MyFakeID", "name": "MyID"}} - # mock list_flavors() to return empty list - mock_function = mock.Mock(return_value={"flavors": []}) - self.useFixture(MonkeyPatch(self.CLIENT_MOCK + '.list_flavors', - mock_function)) - self._mock_find_or_create_flavor(return_value=return_value, - func2mock='.create_flavor', - flavor_name="MyID", - expected_resp="MyFakeID", - allow_creation=True) - - def test_find_flavor_by_id(self): - return_value = {"flavors": self.FLAVORS_LIST} - self._mock_find_or_create_flavor(return_value=return_value, - func2mock='.list_flavors', - flavor_id="MyFakeID", - flavor_name=None, - expected_resp="MyFakeID") - - def test_find_flavor_by_name(self): - return_value = {"flavors": self.FLAVORS_LIST} - self._mock_find_or_create_flavor(return_value=return_value, - func2mock='.list_flavors', - flavor_name="MyID", - expected_resp="MyFakeID") diff --git a/config_tempest/tests/test_config_tempest_network.py b/config_tempest/tests/test_config_tempest_network.py deleted file mode 100644 index aa7b81e5..00000000 --- a/config_tempest/tests/test_config_tempest_network.py +++ /dev/null @@ -1,134 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright 2017 Red Hat, Inc. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from fixtures import MonkeyPatch -import mock - -from config_tempest import main as tool -from config_tempest.tests.base import BaseConfigTempestTest - - -class TestCreateTempestNetworks(BaseConfigTempestTest): - - FAKE_NETWORK_LIST = { - 'networks': [{ - 'provider:physical_network': None, - 'id': '1ea533d7-4c65-4f25', - 'router:external': True, - 'availability_zone_hints': [], - 'availability_zones': [], - 'ipv4_address_scope': None, - 'status': 'ACTIVE', - 'subnets': ['fake_subnet'], - 'label': 'my_fake_label', - 'name': 'tempest-network', - 'admin_state_up': True, - }] - } - - NEUTRON_CLIENT_MOCK = ('tempest.lib.services.network.' - 'networks_client.NetworksClient') - NOVA_CLIENT_MOCK = ('tempest.lib.services.compute.' - 'networks_client.NetworksClient') - - def setUp(self): - super(TestCreateTempestNetworks, self).setUp() - self.conf = self._get_conf("v2.0", "v3") - self.clients = self._get_clients(self.conf) - - def _mock_list_networks(self, client, client_mock, return_value): - mock_function = mock.Mock(return_value=return_value) - client_mock = client_mock + '.list_networks' - self.useFixture(MonkeyPatch(client_mock, mock_function)) - - def test_tempest_network_id_not_found(self): - neutron_client = self.clients.get_neutron_client() - self._mock_list_networks(neutron_client, - self.NEUTRON_CLIENT_MOCK, - return_value={"networks": []}) - self.assertRaises(ValueError, - tool.create_tempest_networks, - clients=self.clients, - conf=self.conf, - has_neutron=True, - public_network_id="doesn't_exist") - - def test_create_network_id_supplied_by_user(self): - neutron_client = self.clients.get_neutron_client() - self._mock_list_networks(neutron_client, - self.NEUTRON_CLIENT_MOCK, - return_value=self.FAKE_NETWORK_LIST) - tool.create_tempest_networks(clients=self.clients, - conf=self.conf, - has_neutron=True, - public_network_id='1ea533d7-4c65-4f25') - self.assertEqual(self.conf.get('network', 'public_network_id'), - '1ea533d7-4c65-4f25') - - def test_create_network_auto_discover(self): - neutron_client = self.clients.get_neutron_client() - self._mock_list_networks(neutron_client, - self.NEUTRON_CLIENT_MOCK, - return_value=self.FAKE_NETWORK_LIST) - tool.create_tempest_networks(clients=self.clients, - conf=self.conf, - has_neutron=True, - public_network_id=None) - self.assertEqual(self.conf.get('network', 'public_network_id'), - '1ea533d7-4c65-4f25') - self.assertEqual(self.conf.get('network', 'floating_network_name'), - 'tempest-network') - - @mock.patch('config_tempest.main.LOG') - def test_create_network_auto_discover_not_found(self, mock_logging): - neutron_client = self.clients.get_neutron_client() - # delete subnets => network will not be found - self.FAKE_NETWORK_LIST['networks'][0]['subnets'] = [] - self._mock_list_networks(neutron_client, - self.NEUTRON_CLIENT_MOCK, - return_value=self.FAKE_NETWORK_LIST) - tool.create_tempest_networks(clients=self.clients, - conf=self.conf, - has_neutron=True, - public_network_id=None) - # check if LOG.error was called - self.assertTrue(mock_logging.error.called) - - def test_network_not_discovered(self): - exception = Exception - nova_client = self.clients.get_nova_net_client() - self._mock_list_networks(nova_client, - self.NOVA_CLIENT_MOCK, - return_value=False) - self.assertRaises(exception, - tool.create_tempest_networks, - clients=self.clients, - conf=self.conf, - has_neutron=False, - public_network_id=None) - - def test_create_fixed_network(self): - nova_client = self.clients.get_nova_net_client() - self._mock_list_networks(nova_client, - self.NOVA_CLIENT_MOCK, - return_value=self.FAKE_NETWORK_LIST) - tool.create_tempest_networks(clients=self.clients, - conf=self.conf, - has_neutron=False, - public_network_id=None) - self.assertEqual(self.conf.get('compute', 'fixed_network_name'), - 'my_fake_label') diff --git a/config_tempest/tests/test_credentials.py b/config_tempest/tests/test_credentials.py index 0b4a8750..962efe65 100644 --- a/config_tempest/tests/test_credentials.py +++ b/config_tempest/tests/test_credentials.py @@ -28,6 +28,28 @@ class TestCredentials(BaseConfigTempestTest): self.conf = self._get_conf("v2.0", "v3") self.creds = self._get_creds(self.conf) + def test_get_credential(self): + # set conf containing the newer values (admin creds in auth section) + self.creds._conf = self._get_alt_conf("v2.0", "v3") + resp = self.creds.get_credential("username") + self.assertEqual(resp, "demo") + # set admin credentials + self.creds.admin = True + resp = self.creds.get_credential("username") + self.assertEqual(resp, "admin") + + def test_get_identity_credential(self): + for i in range(0, 2): + resp = self.creds.get_identity_credential("username") + self.assertEqual(resp, "demo") + # set admin credentials + self.creds.admin = True + resp = self.creds.get_identity_credential("admin_username") + self.assertEqual(resp, "admin") + # use conf which contains the newer values - (admin creds + # in auth section) + self.creds._conf = self._get_alt_conf("v2.0", "v3") + def test_get_identity_version_v2(self): resp = self.creds._get_identity_version() self.assertEqual(resp, 'v2') @@ -38,6 +60,23 @@ class TestCredentials(BaseConfigTempestTest): resp = creds._get_identity_version() self.assertEqual(resp, 'v3') + def test_get_creds_kwargs(self): + expected_resp = { + 'username': 'demo', + 'password': 'secret', + 'tenant_name': 'demo' + } + self.assertEqual(self.creds._get_creds_kwargs(), expected_resp) + self.creds.identity_version = 'v3' + expected_resp = { + 'username': 'demo', + 'password': 'secret', + 'project_name': 'demo', + 'domain_name': 'Default', + 'user_domain_name': 'Default' + } + self.assertEqual(self.creds._get_creds_kwargs(), expected_resp) + def test_set_credentials_v2(self): mock_function = mock.Mock() function2mock = 'config_tempest.credentials.auth.get_credentials' diff --git a/config_tempest/tests/test_flavors.py b/config_tempest/tests/test_flavors.py new file mode 100644 index 00000000..6417ea2c --- /dev/null +++ b/config_tempest/tests/test_flavors.py @@ -0,0 +1,112 @@ +# Copyright 2018 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from fixtures import MonkeyPatch +import mock + +from config_tempest.flavors import Flavors +from config_tempest.tests.base import BaseConfigTempestTest + + +class TestFlavors(BaseConfigTempestTest): + """Flavors test class + + Tests for create_tempest_flavors and find_or_create_flavor methods. + """ + + CLIENT_MOCK = 'tempest.lib.services.compute.flavors_client.FlavorsClient' + FLAVORS_LIST = [ + {"id": "Fakeid", "name": "Name"}, + {"id": "MyFakeID", "name": "MyID"} + ] + + def setUp(self): + super(TestFlavors, self).setUp() + self.conf = self._get_conf("v2.0", "v3") + self.client = self._get_clients(self.conf).flavors + self.Service = Flavors(self.client, True, self.conf) + + def _mock_create_tempest_flavor(self, mock_function): + func2mock = 'config_tempest.flavors.Flavors.find_or_create_flavor' + self.useFixture(MonkeyPatch(func2mock, mock_function)) + self.Service.create_tempest_flavors() + + def _mock_find_or_create_flavor(self, return_value, func2mock, flavor_name, + expected_resp, allow_creation=False, + flavor_id=None): + self.Service.allow_creation = allow_creation + mock_function = mock.Mock(return_value=return_value) + self.useFixture(MonkeyPatch(self.CLIENT_MOCK + func2mock, + mock_function)) + resp = self.Service.find_or_create_flavor(flavor_id=flavor_id, + flavor_name=flavor_name) + self.assertEqual(resp, expected_resp) + + def test_create_tempest_flavors(self): + mock_function = mock.Mock(return_value="FakeID") + self._mock_create_tempest_flavor(mock_function) + self.assertEqual(self.conf.get('compute', 'flavor_ref'), "FakeID") + self.assertEqual(self.conf.get('compute', 'flavor_ref_alt'), "FakeID") + calls = [mock.call(None, 'm1.nano', ram=64), + mock.call(None, 'm1.micro', ram=128)] + mock_function.assert_has_calls(calls, any_order=True) + + def test_create_tempest_flavors_overwrite(self): + mock_function = mock.Mock(return_value="FakeID") + self.conf.set('compute', 'flavor_ref', "FAKE_ID") + self.conf.set('compute', 'flavor_ref_alt', "FAKE_ID") + self._mock_create_tempest_flavor(mock_function) + calls = [mock.call("FAKE_ID", 'm1.nano', ram=64), + mock.call("FAKE_ID", 'm1.micro', ram=128)] + mock_function.assert_has_calls(calls, any_order=True) + + def test_create_flavor_not_allowed(self): + # mock list_flavors() to return empty list + self.Service.allow_creation = False + mock_function = mock.Mock(return_value={"flavors": []}) + self.useFixture(MonkeyPatch(self.CLIENT_MOCK + '.list_flavors', + mock_function)) + exc = Exception + self.assertRaises(exc, + self.Service.find_or_create_flavor, + flavor_id="id", + flavor_name="name") + + def test_create_flavor(self): + return_value = {"flavor": {"id": "MyFakeID", "name": "MyID"}} + # mock list_flavors() to return empty list + mock_function = mock.Mock(return_value={"flavors": []}) + self.useFixture(MonkeyPatch(self.CLIENT_MOCK + '.list_flavors', + mock_function)) + self._mock_find_or_create_flavor(return_value=return_value, + func2mock='.create_flavor', + flavor_name="MyID", + expected_resp="MyFakeID", + allow_creation=True) + + def test_find_flavor_by_id(self): + return_value = {"flavors": self.FLAVORS_LIST} + self._mock_find_or_create_flavor(return_value=return_value, + func2mock='.list_flavors', + flavor_id="MyFakeID", + flavor_name=None, + expected_resp="MyFakeID") + + def test_find_flavor_by_name(self): + return_value = {"flavors": self.FLAVORS_LIST} + self._mock_find_or_create_flavor(return_value=return_value, + func2mock='.list_flavors', + flavor_name="MyID", + expected_resp="MyFakeID") diff --git a/config_tempest/tests/test_config_tempest_user.py b/config_tempest/tests/test_users.py similarity index 71% rename from config_tempest/tests/test_config_tempest_user.py rename to config_tempest/tests/test_users.py index 254d9e75..9a2b74d4 100644 --- a/config_tempest/tests/test_config_tempest_user.py +++ b/config_tempest/tests/test_users.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- - -# Copyright 2017 Red Hat, Inc. +# Copyright 2018 Red Hat, Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -17,98 +15,80 @@ import mock -from config_tempest import main as tool from config_tempest.tests.base import BaseConfigTempestTest +from config_tempest.users import Users from tempest.lib import exceptions -class TestCreateTempestUser(BaseConfigTempestTest): +class TestUsers(BaseConfigTempestTest): def setUp(self): - super(TestCreateTempestUser, self).setUp() + super(TestUsers, self).setUp() self.conf = self._get_conf("v2.0", "v3") - self.tenants_client = self._get_clients(self.conf).tenants - self.users_client = self._get_clients(self.conf).users - self.roles_client = self._get_clients(self.conf).roles + tenants_client = self._get_clients(self.conf).tenants + users_client = self._get_clients(self.conf).users + roles_client = self._get_clients(self.conf).roles + self.Service = Users(tenants_client, roles_client, + users_client, self.conf) + self.username = "test_user" + self.password = "cryptic" + self.tenant_name = "project" + self.tenant_description = "Tenant for Tempest %s user" % self.username + self.role_name = "fake_role" + self.email = "%s@test.com" % self.username + self.users = {'users': + [{'name': "test_user", + 'id': "fake_user_id"}, + {'name': "test_user2", + 'id': "fake_user_id2"}]} + self.roles = {'roles': + [{'name': "fake_role", + 'id': "fake_role_id"}, + {'name': "fake_role2", + 'id': "fake_role_id2"}]} - @mock.patch('config_tempest.main.create_user_with_tenant') - @mock.patch('config_tempest.main.give_role_to_user') + @mock.patch('config_tempest.users.Users.' + 'create_user_with_tenant') + @mock.patch('config_tempest.users.Users.give_role_to_user') def _test_create_tempest_user(self, mock_give_role_to_user, mock_create_user_with_tenant, - services): + orchestration=False): alt_username = "my_user" alt_password = "my_pass" alt_tenant_name = "my_tenant" self.conf.set("identity", "alt_username", alt_username) self.conf.set("identity", "alt_password", alt_password) self.conf.set("identity", "alt_tenant_name", alt_tenant_name) - tool.create_tempest_users(self.tenants_client, - self.roles_client, - self.users_client, - self.conf, - services=services) - if 'orchestration' in services: + self.Service.create_tempest_users(orchestration) + if orchestration: self.assertEqual(mock_give_role_to_user.mock_calls, [ - mock.call(self.tenants_client, - self.roles_client, - self.users_client, - self.conf.get('identity', + mock.call(self.conf.get('identity', 'admin_username'), - self.conf.get('identity', - 'tenant_name'), role_name='admin'), - mock.call(self.tenants_client, - self.roles_client, - self.users_client, - self.conf.get('identity', + mock.call(self.conf.get('identity', 'username'), - self.conf.get('identity', - 'tenant_name'), role_name='heat_stack_owner', role_required=False), ]) else: mock_give_role_to_user.assert_called_with( - self.tenants_client, self.roles_client, - self.users_client, self.conf.get('identity', 'admin_username'), - self.conf.get('identity', 'tenant_name'), role_name='admin') self.assertEqual(mock_create_user_with_tenant.mock_calls, [ - mock.call(self.tenants_client, - self.users_client, - self.conf.get('identity', 'username'), + mock.call(self.conf.get('identity', 'username'), self.conf.get('identity', 'password'), self.conf.get('identity', 'tenant_name')), - mock.call(self.tenants_client, - self.users_client, - self.conf.get('identity', 'alt_username'), + mock.call(self.conf.get('identity', 'alt_username'), self.conf.get('identity', 'alt_password'), self.conf.get('identity', 'alt_tenant_name')), ]) def test_create_tempest_user(self): - services = ['compute', 'network'] - self._test_create_tempest_user(services=services) + self._test_create_tempest_user(orchestration=False) def test_create_tempest_user_with_orchestration(self): - services = ['compute', 'network', 'orchestration'] - self._test_create_tempest_user(services=services) - - -class TestCreateUserWithTenant(BaseConfigTempestTest): - - def setUp(self): - super(TestCreateUserWithTenant, self).setUp() - self.conf = self._get_conf("v2.0", "v3") - self.tenants_client = self._get_clients(self.conf).tenants - self.users_client = self._get_clients(self.conf).users - self.username = "test_user" - self.password = "cryptic" - self.tenant_name = "project" - self.tenant_description = "Tenant for Tempest %s user" % self.username - self.email = "%s@test.com" % self.username + self._test_create_tempest_user(orchestration=True) @mock.patch('config_tempest.clients.ProjectsClient' '.get_project_by_name') @@ -120,9 +100,7 @@ class TestCreateUserWithTenant(BaseConfigTempestTest): mock_create_project, mock_get_project_by_name): mock_get_project_by_name.return_value = {'id': "fake-id"} - tool.create_user_with_tenant( - tenants_client=self.tenants_client, - users_client=self.users_client, + self.Service.create_user_with_tenant( username=self.username, password=self.password, tenant_name=self.tenant_name) @@ -146,9 +124,7 @@ class TestCreateUserWithTenant(BaseConfigTempestTest): mock_get_project_by_name.return_value = {'id': "fake-id"} exc = exceptions.Conflict mock_create_project.side_effect = exc - tool.create_user_with_tenant( - tenants_client=self.tenants_client, - users_client=self.users_client, + self.Service.create_user_with_tenant( username=self.username, password=self.password, tenant_name=self.tenant_name) @@ -160,8 +136,6 @@ class TestCreateUserWithTenant(BaseConfigTempestTest): tenantId="fake-id", email=self.email) - @mock.patch('tempest.lib.services.identity.v2.' - 'users_client.UsersClient.update_user_password') @mock.patch('tempest.common.identity.get_user_by_username') @mock.patch('config_tempest.clients.ProjectsClient.' 'get_project_by_name') @@ -172,16 +146,13 @@ class TestCreateUserWithTenant(BaseConfigTempestTest): def test_create_user_with_tenant_user_exists( self, mock_create_user, mock_create_tenant, mock_get_project_by_name, - mock_get_user_by_username, - mock_update_user_password): + mock_get_user_by_username): mock_get_project_by_name.return_value = {'id': "fake-id"} exc = exceptions.Conflict mock_create_user.side_effect = exc fake_user = {'id': "fake_user_id"} mock_get_user_by_username.return_value = fake_user - tool.create_user_with_tenant( - tenants_client=self.tenants_client, - users_client=self.users_client, + self.Service.create_user_with_tenant( username=self.username, password=self.password, tenant_name=self.tenant_name) mock_create_tenant.assert_called_with( @@ -191,8 +162,6 @@ class TestCreateUserWithTenant(BaseConfigTempestTest): tenantId="fake-id", email=self.email) - @mock.patch('tempest.lib.services.identity.v2.' - 'users_client.UsersClient.update_user_password') @mock.patch('tempest.common.identity.get_user_by_username') @mock.patch('config_tempest.clients.ProjectsClient.' 'get_project_by_name') @@ -202,19 +171,16 @@ class TestCreateUserWithTenant(BaseConfigTempestTest): def test_create_user_with_tenant_exists_user_exists( self, mock_create_user, mock_create_project, mock_get_project_by_name, - mock_get_user_by_username, - mock_update_user_password): + mock_get_user_by_username): mock_get_project_by_name.return_value = {'id': "fake-id"} exc = exceptions.Conflict mock_create_project.side_effects = exc mock_create_user.side_effect = exc fake_user = {'id': "fake_user_id"} mock_get_user_by_username.return_value = fake_user - tool.create_user_with_tenant(tenants_client=self.tenants_client, - users_client=self.users_client, - username=self.username, - password=self.password, - tenant_name=self.tenant_name) + self.Service.create_user_with_tenant(username=self.username, + password=self.password, + tenant_name=self.tenant_name) mock_create_project.assert_called_with( name=self.tenant_name, description=self.tenant_description) mock_create_user.assert_called_with(name=self.username, @@ -222,29 +188,6 @@ class TestCreateUserWithTenant(BaseConfigTempestTest): tenantId="fake-id", email=self.email) - -class TestGiveRoleToUser(BaseConfigTempestTest): - - def setUp(self): - super(TestGiveRoleToUser, self).setUp() - self.conf = self._get_conf("v2.0", "v3") - self.tenants_client = self._get_clients(self.conf).tenants - self.users_client = self._get_clients(self.conf).users - self.roles_client = self._get_clients(self.conf).roles - self.username = "test_user" - self.tenant_name = "project" - self.role_name = "fake_role" - self.users = {'users': - [{'name': "test_user", - 'id': "fake_user_id"}, - {'name': "test_user2", - 'id': "fake_user_id2"}]} - self.roles = {'roles': - [{'name': "fake_role", - 'id': "fake_role_id"}, - {'name': "fake_role2", - 'id': "fake_role_id2"}]} - @mock.patch('config_tempest.clients.ProjectsClient.' 'get_project_by_name') @mock.patch('tempest.lib.services.identity.v2.' @@ -266,12 +209,8 @@ class TestGiveRoleToUser(BaseConfigTempestTest): {'id': "fake_tenant_id"} mock_list_users.return_value = self.users mock_list_roles.return_value = self.roles - tool.give_role_to_user( - tenants_client=self.tenants_client, - roles_client=self.roles_client, - users_client=self.users_client, + self.Service.give_role_to_user( username=self.username, - tenant_name=self.tenant_name, role_name=self.role_name) mock_create_user_role_on_project.assert_called_with( "fake_tenant_id", "fake_user_id", "fake_role_id") @@ -300,12 +239,8 @@ class TestGiveRoleToUser(BaseConfigTempestTest): mock_list_roles.return_value = self.roles exc = Exception self.assertRaises(exc, - tool.give_role_to_user, - tenants_client=self.tenants_client, - roles_client=self.roles_client, - users_client=self.users_client, + self.Service.give_role_to_user, username=self.username, - tenant_name=self.tenant_name, role_name=role_name) @mock.patch('config_tempest.clients.ProjectsClient.' @@ -330,12 +265,8 @@ class TestGiveRoleToUser(BaseConfigTempestTest): {'id': "fake_tenant_id"} mock_list_users.return_value = self.users mock_list_roles.return_value = self.roles - tool.give_role_to_user( - tenants_client=self.tenants_client, - roles_client=self.roles_client, - users_client=self.users_client, + self.Service.give_role_to_user( username=self.username, - tenant_name=self.tenant_name, role_name=self.role_name, role_required=False) @@ -361,10 +292,6 @@ class TestGiveRoleToUser(BaseConfigTempestTest): mock_get_project_by_name.return_value = {'id': "fake_tenant_id"} mock_list_users.return_value = self.users mock_list_roles.return_value = self.roles - tool.give_role_to_user( - tenants_client=self.tenants_client, - roles_client=self.roles_client, - users_client=self.users_client, + self.Service.give_role_to_user( username=self.username, - tenant_name=self.tenant_name, role_name=self.role_name) diff --git a/config_tempest/users.py b/config_tempest/users.py new file mode 100644 index 00000000..cd4969c6 --- /dev/null +++ b/config_tempest/users.py @@ -0,0 +1,118 @@ +# Copyright 2016, 2018 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from constants import LOG +from tempest.lib import exceptions + + +class Users(object): + def __init__(self, tenants_client, roles_client, users_client, conf): + """Init. + + :type tenants_client: ProjectsClient object + :type roles_client: RolesClient object from tempest lib + :type users_client: UsersClient object from tempest lib + :type conf: TempestConf object + """ + self.tenants_client = tenants_client + self.roles_client = roles_client + self.users_client = users_client + self._conf = conf + + def create_tempest_users(self, orchestration=False): + """Create users necessary for Tempest if they don't exist already. + + :type orchestration: boolean + """ + sec = 'identity' + self.create_user_with_tenant(self._conf.get(sec, 'username'), + self._conf.get(sec, 'password'), + self._conf.get(sec, 'tenant_name')) + + self.create_user_with_tenant(self._conf.get(sec, 'alt_username'), + self._conf.get(sec, 'alt_password'), + self._conf.get(sec, 'alt_tenant_name')) + + username = self._conf.get_defaulted('auth', 'admin_username') + if username is None: + username = self._conf.get_defaulted('identity', 'admin_username') + + self.give_role_to_user(username, role_name='admin') + + # Prior to juno, and with earlier juno defaults, users needed to have + # the heat_stack_owner role to use heat stack apis. We assign that role + # to the user if the role is present. + if orchestration: + self.give_role_to_user(self._conf.get('identity', 'username'), + role_name='heat_stack_owner', + role_required=False) + + def give_role_to_user(self, username, role_name, + role_required=True): + """Give the user a role in the project (tenant). + + :type username: string + :type role_name: string + :type role_required: boolean + """ + tenant_name = self._conf.get('identity', 'tenant_name') + tenant_id = self.tenants_client.get_project_by_name(tenant_name)['id'] + users = self.users_client.list_users() + user_ids = [u['id'] for u in users['users'] if u['name'] == username] + user_id = user_ids[0] + roles = self.roles_client.list_roles() + role_ids = [r['id'] for r in roles['roles'] if r['name'] == role_name] + if not role_ids: + if role_required: + raise Exception("required role %s not found" % role_name) + LOG.debug("%s role not required", role_name) + return + role_id = role_ids[0] + try: + self.roles_client.create_user_role_on_project(tenant_id, user_id, + role_id) + LOG.debug("User '%s' was given the '%s' role in project '%s'", + username, role_name, tenant_name) + except exceptions.Conflict: + LOG.debug("(no change) User '%s' already has the '%s' role in" + " project '%s'", username, role_name, tenant_name) + + def create_user_with_tenant(self, username, password, tenant_name): + """Create a user and a tenant if it doesn't exist. + + :type username: string + :type password: string + :type tenant_name: string + """ + LOG.info("Creating user '%s' with tenant '%s' and password '%s'", + username, tenant_name, password) + tenant_description = "Tenant for Tempest %s user" % username + email = "%s@test.com" % username + # create a tenant + try: + self.tenants_client.create_project(name=tenant_name, + description=tenant_description) + except exceptions.Conflict: + LOG.info("(no change) Tenant '%s' already exists", tenant_name) + + tenant_id = self.tenants_client.get_project_by_name(tenant_name)['id'] + + params = {'name': username, 'password': password, + 'tenantId': tenant_id, 'email': email} + # create a user + try: + self.users_client.create_user(**params) + except exceptions.Conflict: + LOG.info("User '%s' already exists.", username)