[verification] Introduce Tempest verifier
Co-Authored-By: Yaroslav Lobankov <ylobankov@mirantis.com> Change-Id: I06c7e1d2c7956a1ad92e62484d84141ee926be08
This commit is contained in:
parent
a2dba3c333
commit
82b6cccd71
@ -24,27 +24,6 @@ for each enum. (e.g TaskStatus)
|
|||||||
from rally.common import utils
|
from rally.common import utils
|
||||||
|
|
||||||
|
|
||||||
class _TempestTestsAPI(utils.ImmutableMixin, utils.EnumMixin):
|
|
||||||
BAREMETAL = "baremetal"
|
|
||||||
CLUSTERING = "clustering"
|
|
||||||
COMPUTE = "compute"
|
|
||||||
DATA_PROCESSING = "data_processing"
|
|
||||||
DATABASE = "database"
|
|
||||||
IDENTITY = "identity"
|
|
||||||
IMAGE = "image"
|
|
||||||
MESSAGING = "messaging"
|
|
||||||
NETWORK = "network"
|
|
||||||
OBJECT_STORAGE = "object_storage"
|
|
||||||
ORCHESTRATION = "orchestration"
|
|
||||||
TELEMETRY = "telemetry"
|
|
||||||
VOLUME = "volume"
|
|
||||||
|
|
||||||
|
|
||||||
class _TempestTestsSets(utils.ImmutableMixin, utils.EnumMixin):
|
|
||||||
FULL = "full"
|
|
||||||
SMOKE = "smoke"
|
|
||||||
SCENARIO = "scenario"
|
|
||||||
|
|
||||||
JSON_SCHEMA = "http://json-schema.org/draft-04/schema"
|
JSON_SCHEMA = "http://json-schema.org/draft-04/schema"
|
||||||
|
|
||||||
|
|
||||||
@ -239,8 +218,6 @@ EndpointPermission = _EndpointPermission()
|
|||||||
ServiceType = _ServiceType()
|
ServiceType = _ServiceType()
|
||||||
Service = _Service()
|
Service = _Service()
|
||||||
EndpointType = _EndpointType()
|
EndpointType = _EndpointType()
|
||||||
TempestTestsAPI = _TempestTestsAPI()
|
|
||||||
TempestTestsSets = _TempestTestsSets()
|
|
||||||
HookStatus = _HookStatus()
|
HookStatus = _HookStatus()
|
||||||
TagType = _TagType()
|
TagType = _TagType()
|
||||||
VerifierStatus = _VerifierStatus()
|
VerifierStatus = _VerifierStatus()
|
||||||
|
0
rally/plugins/openstack/verification/__init__.py
Normal file
0
rally/plugins/openstack/verification/__init__.py
Normal file
55
rally/plugins/openstack/verification/tempest/config.ini
Normal file
55
rally/plugins/openstack/verification/tempest/config.ini
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
[DEFAULT]
|
||||||
|
debug = True
|
||||||
|
log_file = tempest.log
|
||||||
|
use_stderr = False
|
||||||
|
|
||||||
|
[auth]
|
||||||
|
use_dynamic_credentials = True
|
||||||
|
|
||||||
|
[compute]
|
||||||
|
image_ref =
|
||||||
|
image_ref_alt =
|
||||||
|
flavor_ref =
|
||||||
|
flavor_ref_alt =
|
||||||
|
fixed_network_name =
|
||||||
|
|
||||||
|
[compute-feature-enabled]
|
||||||
|
live_migration = False
|
||||||
|
resize = True
|
||||||
|
vnc_console = True
|
||||||
|
attach_encrypted_volume = False
|
||||||
|
|
||||||
|
[data-processing]
|
||||||
|
|
||||||
|
[identity]
|
||||||
|
|
||||||
|
[image-feature-enabled]
|
||||||
|
deactivate_image = True
|
||||||
|
|
||||||
|
[input-scenario]
|
||||||
|
ssh_user_regex = [["^.*[Cc]irros.*$", "cirros"], ["^.*[Tt]est[VvMm].*$", "cirros"], ["^.*rally_verify.*$", "cirros"]]
|
||||||
|
|
||||||
|
[network]
|
||||||
|
|
||||||
|
[network-feature-enabled]
|
||||||
|
ipv6_subnet_attributes = True
|
||||||
|
ipv6 = True
|
||||||
|
|
||||||
|
[object-storage]
|
||||||
|
|
||||||
|
[oslo_concurrency]
|
||||||
|
|
||||||
|
[orchestration]
|
||||||
|
instance_type =
|
||||||
|
|
||||||
|
[scenario]
|
||||||
|
img_file =
|
||||||
|
|
||||||
|
[service_available]
|
||||||
|
|
||||||
|
[validation]
|
||||||
|
run_validation = True
|
||||||
|
image_ssh_user = cirros
|
||||||
|
|
||||||
|
[volume-feature-enabled]
|
||||||
|
bootable = True
|
594
rally/plugins/openstack/verification/tempest/config.py
Normal file
594
rally/plugins/openstack/verification/tempest/config.py
Normal file
@ -0,0 +1,594 @@
|
|||||||
|
# Copyright 2014: Mirantis 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 inspect
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
import requests
|
||||||
|
import six
|
||||||
|
from six.moves import configparser
|
||||||
|
from six.moves.urllib import parse
|
||||||
|
|
||||||
|
from rally.common.i18n import _
|
||||||
|
from rally.common import logging
|
||||||
|
from rally.common import objects
|
||||||
|
from rally.common import utils
|
||||||
|
from rally import exceptions
|
||||||
|
from rally import osclients
|
||||||
|
from rally.plugins.openstack.wrappers import glance
|
||||||
|
from rally.plugins.openstack.wrappers import network
|
||||||
|
from rally.task import utils as task_utils
|
||||||
|
from rally.verification import context
|
||||||
|
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
TEMPEST_OPTS = [
|
||||||
|
cfg.StrOpt("img_url",
|
||||||
|
deprecated_opts=[cfg.DeprecatedOpt("cirros_img_url",
|
||||||
|
group="image")],
|
||||||
|
default="http://download.cirros-cloud.net/"
|
||||||
|
"0.3.4/cirros-0.3.4-x86_64-disk.img",
|
||||||
|
help="image URL"),
|
||||||
|
cfg.StrOpt("img_disk_format",
|
||||||
|
deprecated_opts=[cfg.DeprecatedOpt("disk_format",
|
||||||
|
group="image")],
|
||||||
|
default="qcow2",
|
||||||
|
help="Image disk format to use when creating the image"),
|
||||||
|
cfg.StrOpt("img_container_format",
|
||||||
|
deprecated_opts=[cfg.DeprecatedOpt("container_format",
|
||||||
|
group="image")],
|
||||||
|
default="bare",
|
||||||
|
help="Image container format to use when creating the image"),
|
||||||
|
cfg.StrOpt("img_name_regex",
|
||||||
|
deprecated_opts=[cfg.DeprecatedOpt("name_regex",
|
||||||
|
group="image")],
|
||||||
|
default="^.*(cirros|testvm).*$",
|
||||||
|
help="Regular expression for name of a public image to "
|
||||||
|
"discover it in the cloud and use it for the tests. "
|
||||||
|
"Note that when Rally is searching for the image, case "
|
||||||
|
"insensitive matching is performed. Specify nothing "
|
||||||
|
"('img_name_regex =') if you want to disable discovering. "
|
||||||
|
"In this case Rally will create needed resources by "
|
||||||
|
"itself if the values for the corresponding config "
|
||||||
|
"options are not specified in the Tempest config file"),
|
||||||
|
cfg.StrOpt("swift_operator_role",
|
||||||
|
deprecated_group="role",
|
||||||
|
default="Member",
|
||||||
|
help="Role required for users "
|
||||||
|
"to be able to create Swift containers"),
|
||||||
|
cfg.StrOpt("swift_reseller_admin_role",
|
||||||
|
deprecated_group="role",
|
||||||
|
default="ResellerAdmin",
|
||||||
|
help="User role that has reseller admin"),
|
||||||
|
cfg.StrOpt("heat_stack_owner_role",
|
||||||
|
deprecated_group="role",
|
||||||
|
default="heat_stack_owner",
|
||||||
|
help="Role required for users "
|
||||||
|
"to be able to manage Heat stacks"),
|
||||||
|
cfg.StrOpt("heat_stack_user_role",
|
||||||
|
deprecated_group="role",
|
||||||
|
default="heat_stack_user",
|
||||||
|
help="Role for Heat template-defined users"),
|
||||||
|
cfg.IntOpt("flavor_ref_ram",
|
||||||
|
default="64",
|
||||||
|
help="Primary flavor RAM size used by most of the test cases"),
|
||||||
|
cfg.IntOpt("flavor_ref_alt_ram",
|
||||||
|
default="128",
|
||||||
|
help="Alternate reference flavor RAM size used by test that"
|
||||||
|
"need two flavors, like those that resize an instance"),
|
||||||
|
cfg.IntOpt("heat_instance_type_ram",
|
||||||
|
default="64",
|
||||||
|
help="RAM size flavor used for orchestration test cases")
|
||||||
|
]
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
CONF.register_opts(TEMPEST_OPTS, "tempest")
|
||||||
|
CONF.import_opt("glance_image_delete_timeout",
|
||||||
|
"rally.plugins.openstack.scenarios.glance.utils",
|
||||||
|
"benchmark")
|
||||||
|
CONF.import_opt("glance_image_delete_poll_interval",
|
||||||
|
"rally.plugins.openstack.scenarios.glance.utils",
|
||||||
|
"benchmark")
|
||||||
|
|
||||||
|
|
||||||
|
def _create_or_get_data_dir():
|
||||||
|
data_dir = os.path.join(
|
||||||
|
os.path.expanduser("~"), ".rally", "verification", "tempest", "data")
|
||||||
|
if not os.path.exists(data_dir):
|
||||||
|
os.makedirs(data_dir)
|
||||||
|
|
||||||
|
return data_dir
|
||||||
|
|
||||||
|
|
||||||
|
def _download_image(image_path, image=None):
|
||||||
|
if image:
|
||||||
|
LOG.debug("Downloading image '%s' "
|
||||||
|
"from Glance to %s" % (image.name, image_path))
|
||||||
|
with open(image_path, "wb") as image_file:
|
||||||
|
for chunk in image.data():
|
||||||
|
image_file.write(chunk)
|
||||||
|
else:
|
||||||
|
LOG.debug("Downloading image from %s "
|
||||||
|
"to %s" % (CONF.tempest.img_url, image_path))
|
||||||
|
try:
|
||||||
|
response = requests.get(CONF.tempest.img_url, stream=True)
|
||||||
|
except requests.ConnectionError as err:
|
||||||
|
msg = _("Failed to download image. "
|
||||||
|
"Possibly there is no connection to Internet. "
|
||||||
|
"Error: %s.") % (str(err) or "unknown")
|
||||||
|
raise exceptions.TempestConfigCreationFailure(msg)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
with open(image_path, "wb") as image_file:
|
||||||
|
for chunk in response.iter_content(chunk_size=1024):
|
||||||
|
if chunk: # filter out keep-alive new chunks
|
||||||
|
image_file.write(chunk)
|
||||||
|
image_file.flush()
|
||||||
|
else:
|
||||||
|
if response.status_code == 404:
|
||||||
|
msg = _("Failed to download image. Image was not found.")
|
||||||
|
else:
|
||||||
|
msg = _("Failed to download image. "
|
||||||
|
"HTTP error code %d.") % response.status_code
|
||||||
|
raise exceptions.TempestConfigCreationFailure(msg)
|
||||||
|
|
||||||
|
LOG.debug("The image has been successfully downloaded!")
|
||||||
|
|
||||||
|
|
||||||
|
def write_configfile(path, conf_object):
|
||||||
|
with open(path, "w") as configfile:
|
||||||
|
conf_object.write(configfile)
|
||||||
|
|
||||||
|
|
||||||
|
def read_configfile(path):
|
||||||
|
with open(path) as f:
|
||||||
|
return f.read()
|
||||||
|
|
||||||
|
|
||||||
|
def add_extra_options(extra_options, conf_object):
|
||||||
|
for section in extra_options:
|
||||||
|
if section not in (conf_object.sections() + ["DEFAULT"]):
|
||||||
|
conf_object.add_section(section)
|
||||||
|
for option, value in extra_options[section].items():
|
||||||
|
conf_object.set(section, option, value)
|
||||||
|
|
||||||
|
return conf_object
|
||||||
|
|
||||||
|
|
||||||
|
def extend_configfile(configfile, extra_options):
|
||||||
|
conf = configparser.ConfigParser()
|
||||||
|
|
||||||
|
conf.read(configfile)
|
||||||
|
add_extra_options(extra_options, conf)
|
||||||
|
write_configfile(configfile, conf)
|
||||||
|
raw_conf = six.StringIO()
|
||||||
|
conf.write(raw_conf)
|
||||||
|
return raw_conf.getvalue()
|
||||||
|
|
||||||
|
|
||||||
|
class TempestConfigfileManager(utils.RandomNameGeneratorMixin):
|
||||||
|
"""Class to create a Tempest config file."""
|
||||||
|
|
||||||
|
def __init__(self, deployment):
|
||||||
|
self.deployment = deployment
|
||||||
|
|
||||||
|
self.credential = deployment["admin"]
|
||||||
|
self.clients = osclients.Clients(objects.Credential(**self.credential))
|
||||||
|
self.keystone = self.clients.verified_keystone()
|
||||||
|
self.available_services = self.clients.services().values()
|
||||||
|
|
||||||
|
self.data_dir = _create_or_get_data_dir()
|
||||||
|
|
||||||
|
self.conf = configparser.ConfigParser()
|
||||||
|
self.conf.read(os.path.join(os.path.dirname(__file__), "config.ini"))
|
||||||
|
|
||||||
|
def _get_service_url(self, service_name):
|
||||||
|
s_type = self._get_service_type_by_service_name(service_name)
|
||||||
|
available_endpoints = self.keystone.service_catalog.get_endpoints()
|
||||||
|
service_endpoints = available_endpoints.get(s_type, [])
|
||||||
|
for endpoint in service_endpoints:
|
||||||
|
# If endpoints were returned by Keystone API V2
|
||||||
|
if "publicURL" in endpoint:
|
||||||
|
return endpoint["publicURL"]
|
||||||
|
# If endpoints were returned by Keystone API V3
|
||||||
|
if endpoint["interface"] == "public":
|
||||||
|
return endpoint["url"]
|
||||||
|
|
||||||
|
def _get_service_type_by_service_name(self, service_name):
|
||||||
|
for s_type, s_name in self.clients.services().items():
|
||||||
|
if s_name == service_name:
|
||||||
|
return s_type
|
||||||
|
|
||||||
|
def _configure_auth(self, section_name="auth"):
|
||||||
|
self.conf.set(section_name, "admin_username",
|
||||||
|
self.credential["username"])
|
||||||
|
self.conf.set(section_name, "admin_password",
|
||||||
|
self.credential["password"])
|
||||||
|
self.conf.set(section_name, "admin_project_name",
|
||||||
|
self.credential["tenant_name"])
|
||||||
|
# Keystone v3 related parameter
|
||||||
|
self.conf.set(section_name, "admin_domain_name",
|
||||||
|
self.credential["user_domain_name"] or "Default")
|
||||||
|
|
||||||
|
# Sahara has two service types: 'data_processing' and 'data-processing'.
|
||||||
|
# 'data_processing' is deprecated, but it can be used in previous OpenStack
|
||||||
|
# releases. So we need to configure the 'catalog_type' option to support
|
||||||
|
# environments where 'data_processing' is used as service type for Sahara.
|
||||||
|
def _configure_data_processing(self, section_name="data-processing"):
|
||||||
|
if "sahara" in self.available_services:
|
||||||
|
self.conf.set(section_name, "catalog_type",
|
||||||
|
self._get_service_type_by_service_name("sahara"))
|
||||||
|
|
||||||
|
def _configure_identity(self, section_name="identity"):
|
||||||
|
self.conf.set(section_name, "region",
|
||||||
|
self.credential["region_name"])
|
||||||
|
|
||||||
|
auth_url = self.credential["auth_url"]
|
||||||
|
if "/v2" not in auth_url and "/v3" not in auth_url:
|
||||||
|
auth_version = "v2"
|
||||||
|
auth_url_v2 = parse.urljoin(auth_url, "/v2.0")
|
||||||
|
else:
|
||||||
|
url_path = parse.urlparse(auth_url).path
|
||||||
|
auth_version = url_path[1:3]
|
||||||
|
auth_url_v2 = auth_url.replace(url_path, "/v2.0")
|
||||||
|
self.conf.set(section_name, "auth_version", auth_version)
|
||||||
|
self.conf.set(section_name, "uri", auth_url_v2)
|
||||||
|
self.conf.set(section_name, "uri_v3",
|
||||||
|
auth_url_v2.replace("/v2.0", "/v3"))
|
||||||
|
|
||||||
|
self.conf.set(section_name, "disable_ssl_certificate_validation",
|
||||||
|
str(self.credential["https_insecure"]))
|
||||||
|
self.conf.set(section_name, "ca_certificates_file",
|
||||||
|
self.credential["https_cacert"])
|
||||||
|
|
||||||
|
# The compute section is configured in context class for Tempest resources.
|
||||||
|
# Options which are configured there: 'image_ref', 'image_ref_alt',
|
||||||
|
# 'flavor_ref', 'flavor_ref_alt'.
|
||||||
|
|
||||||
|
def _configure_network(self, section_name="network"):
|
||||||
|
if "neutron" in self.available_services:
|
||||||
|
neutronclient = self.clients.neutron()
|
||||||
|
public_nets = [net for net
|
||||||
|
in neutronclient.list_networks()["networks"]
|
||||||
|
if net["status"] == "ACTIVE" and
|
||||||
|
net["router:external"] is True]
|
||||||
|
if public_nets:
|
||||||
|
net_id = public_nets[0]["id"]
|
||||||
|
self.conf.set(section_name, "public_network_id", net_id)
|
||||||
|
else:
|
||||||
|
novaclient = self.clients.nova()
|
||||||
|
net_name = next(net.human_id for net in novaclient.networks.list()
|
||||||
|
if net.human_id is not None)
|
||||||
|
self.conf.set("compute", "fixed_network_name", net_name)
|
||||||
|
self.conf.set("validation", "network_for_ssh", net_name)
|
||||||
|
|
||||||
|
def _configure_network_feature_enabled(
|
||||||
|
self, section_name="network-feature-enabled"):
|
||||||
|
if "neutron" in self.available_services:
|
||||||
|
neutronclient = self.clients.neutron()
|
||||||
|
extensions = neutronclient.list_ext("extensions", "/extensions",
|
||||||
|
retrieve_all=True)
|
||||||
|
aliases = [ext["alias"] for ext in extensions["extensions"]]
|
||||||
|
aliases_str = ",".join(aliases)
|
||||||
|
self.conf.set(section_name, "api_extensions", aliases_str)
|
||||||
|
|
||||||
|
def _configure_oslo_concurrency(self, section_name="oslo_concurrency"):
|
||||||
|
lock_path = os.path.join(self.data_dir,
|
||||||
|
"lock_files_%s" % self.deployment["uuid"])
|
||||||
|
if not os.path.exists(lock_path):
|
||||||
|
os.makedirs(lock_path)
|
||||||
|
self.conf.set(section_name, "lock_path", lock_path)
|
||||||
|
|
||||||
|
def _configure_object_storage(self, section_name="object-storage"):
|
||||||
|
self.conf.set(section_name, "operator_role",
|
||||||
|
CONF.tempest.swift_operator_role)
|
||||||
|
self.conf.set(section_name, "reseller_admin_role",
|
||||||
|
CONF.tempest.swift_reseller_admin_role)
|
||||||
|
|
||||||
|
def _configure_scenario(self, section_name="scenario"):
|
||||||
|
self.conf.set(section_name, "img_dir", self.data_dir)
|
||||||
|
|
||||||
|
def _configure_service_available(self, section_name="service_available"):
|
||||||
|
services = ["cinder", "glance", "heat", "ironic", "neutron", "nova",
|
||||||
|
"sahara", "swift"]
|
||||||
|
for service in services:
|
||||||
|
# Convert boolean to string because ConfigParser fails
|
||||||
|
# on attempt to get option with boolean value
|
||||||
|
self.conf.set(section_name, service,
|
||||||
|
str(service in self.available_services))
|
||||||
|
|
||||||
|
def _configure_validation(self, section_name="validation"):
|
||||||
|
if "neutron" in self.available_services:
|
||||||
|
self.conf.set(section_name, "connect_method", "floating")
|
||||||
|
else:
|
||||||
|
self.conf.set(section_name, "connect_method", "fixed")
|
||||||
|
|
||||||
|
def _configure_orchestration(self, section_name="orchestration"):
|
||||||
|
self.conf.set(section_name, "stack_owner_role",
|
||||||
|
CONF.tempest.heat_stack_owner_role)
|
||||||
|
self.conf.set(section_name, "stack_user_role",
|
||||||
|
CONF.tempest.heat_stack_user_role)
|
||||||
|
|
||||||
|
def create(self, conf_path, extra_options=None):
|
||||||
|
for name, method in inspect.getmembers(self, inspect.ismethod):
|
||||||
|
if name.startswith("_configure_"):
|
||||||
|
method()
|
||||||
|
|
||||||
|
if extra_options:
|
||||||
|
add_extra_options(extra_options, self.conf)
|
||||||
|
|
||||||
|
write_configfile(conf_path, self.conf)
|
||||||
|
|
||||||
|
return read_configfile(conf_path)
|
||||||
|
|
||||||
|
|
||||||
|
@context.configure("tempest_configuration", order=900)
|
||||||
|
class TempestResourcesContext(context.VerifierContext):
|
||||||
|
"""Context class to create/delete resources needed for Tempest."""
|
||||||
|
|
||||||
|
RESOURCE_NAME_FORMAT = "rally_verify_XXXXXXXX_XXXXXXXX"
|
||||||
|
|
||||||
|
def __init__(self, ctx):
|
||||||
|
super(TempestResourcesContext, self).__init__(ctx)
|
||||||
|
credential = self.verifier.deployment["admin"]
|
||||||
|
self.clients = osclients.Clients(objects.Credential(**credential))
|
||||||
|
|
||||||
|
self.conf = configparser.ConfigParser()
|
||||||
|
self.conf_path = self.verifier.manager.configfile
|
||||||
|
self.data_dir = os.path.join(os.path.expanduser("~"), ".rally",
|
||||||
|
"verification", "tempest", "data")
|
||||||
|
self.image_name = "tempest-image"
|
||||||
|
|
||||||
|
self._created_roles = []
|
||||||
|
self._created_images = []
|
||||||
|
self._created_flavors = []
|
||||||
|
self._created_networks = []
|
||||||
|
|
||||||
|
def setup(self):
|
||||||
|
self.available_services = self.clients.services().values()
|
||||||
|
|
||||||
|
self.conf.read(self.conf_path)
|
||||||
|
|
||||||
|
self.data_dir = _create_or_get_data_dir()
|
||||||
|
|
||||||
|
self._create_tempest_roles()
|
||||||
|
|
||||||
|
self._configure_option("scenario", "img_file", self.image_name,
|
||||||
|
helper_method=self._download_image)
|
||||||
|
self._configure_option("compute", "image_ref",
|
||||||
|
helper_method=self._discover_or_create_image)
|
||||||
|
self._configure_option("compute", "image_ref_alt",
|
||||||
|
helper_method=self._discover_or_create_image)
|
||||||
|
self._configure_option("compute", "flavor_ref",
|
||||||
|
helper_method=self._discover_or_create_flavor,
|
||||||
|
flv_ram=CONF.tempest.flavor_ref_ram)
|
||||||
|
self._configure_option("compute", "flavor_ref_alt",
|
||||||
|
helper_method=self._discover_or_create_flavor,
|
||||||
|
flv_ram=CONF.tempest.flavor_ref_alt_ram)
|
||||||
|
if "neutron" in self.available_services:
|
||||||
|
neutronclient = self.clients.neutron()
|
||||||
|
if neutronclient.list_networks(shared=True)["networks"]:
|
||||||
|
# If the OpenStack cloud has some shared networks, we will
|
||||||
|
# create our own shared network and specify its name in the
|
||||||
|
# Tempest config file. Such approach will allow us to avoid
|
||||||
|
# failures of Tempest tests with error "Multiple possible
|
||||||
|
# networks found". Otherwise the default behavior defined in
|
||||||
|
# Tempest will be used and Tempest itself will manage network
|
||||||
|
# resources.
|
||||||
|
LOG.debug("Shared networks found. "
|
||||||
|
"'fixed_network_name' option should be configured")
|
||||||
|
self._configure_option(
|
||||||
|
"compute", "fixed_network_name",
|
||||||
|
helper_method=self._create_network_resources)
|
||||||
|
if "heat" in self.available_services:
|
||||||
|
self._configure_option(
|
||||||
|
"orchestration", "instance_type",
|
||||||
|
helper_method=self._discover_or_create_flavor,
|
||||||
|
flv_ram=CONF.tempest.heat_instance_type_ram)
|
||||||
|
|
||||||
|
write_configfile(self.conf_path, self.conf)
|
||||||
|
|
||||||
|
def cleanup(self):
|
||||||
|
# Tempest tests may take more than 1 hour and we should remove all
|
||||||
|
# cached clients sessions to avoid tokens expiration when deleting
|
||||||
|
# Tempest resources.
|
||||||
|
self.clients.clear()
|
||||||
|
|
||||||
|
self._cleanup_tempest_roles()
|
||||||
|
self._cleanup_images()
|
||||||
|
self._cleanup_flavors()
|
||||||
|
if "neutron" in self.available_services:
|
||||||
|
self._cleanup_network_resources()
|
||||||
|
|
||||||
|
write_configfile(self.conf_path, self.conf)
|
||||||
|
|
||||||
|
def _create_tempest_roles(self):
|
||||||
|
keystoneclient = self.clients.verified_keystone()
|
||||||
|
roles = [CONF.tempest.swift_operator_role,
|
||||||
|
CONF.tempest.swift_reseller_admin_role,
|
||||||
|
CONF.tempest.heat_stack_owner_role,
|
||||||
|
CONF.tempest.heat_stack_user_role]
|
||||||
|
existing_roles = set(role.name for role in keystoneclient.roles.list())
|
||||||
|
|
||||||
|
for role in roles:
|
||||||
|
if role not in existing_roles:
|
||||||
|
LOG.debug("Creating role '%s'" % role)
|
||||||
|
self._created_roles.append(keystoneclient.roles.create(role))
|
||||||
|
|
||||||
|
def _discover_image(self):
|
||||||
|
LOG.debug("Trying to discover a public image with name matching "
|
||||||
|
"regular expression '%s'. Note that case insensitive "
|
||||||
|
"matching is performed." % CONF.tempest.img_name_regex)
|
||||||
|
glance_wrapper = glance.wrap(self.clients.glance, self)
|
||||||
|
images = glance_wrapper.list_images(status="active",
|
||||||
|
visibility="public")
|
||||||
|
for image in images:
|
||||||
|
if image.name and re.match(CONF.tempest.img_name_regex,
|
||||||
|
image.name, re.IGNORECASE):
|
||||||
|
LOG.debug("The following public "
|
||||||
|
"image discovered: '%s'" % image.name)
|
||||||
|
return image
|
||||||
|
|
||||||
|
LOG.debug("There is no public image with name matching "
|
||||||
|
"regular expression '%s'" % CONF.tempest.img_name_regex)
|
||||||
|
|
||||||
|
def _download_image(self):
|
||||||
|
image_path = os.path.join(self.data_dir, self.image_name)
|
||||||
|
if os.path.isfile(image_path):
|
||||||
|
LOG.debug("Image is already downloaded to %s" % image_path)
|
||||||
|
return
|
||||||
|
|
||||||
|
if CONF.tempest.img_name_regex:
|
||||||
|
image = self._discover_image()
|
||||||
|
if image:
|
||||||
|
return _download_image(image_path, image)
|
||||||
|
|
||||||
|
_download_image(image_path)
|
||||||
|
|
||||||
|
def _configure_option(self, section, option, value=None,
|
||||||
|
helper_method=None, *args, **kwargs):
|
||||||
|
option_value = self.conf.get(section, option)
|
||||||
|
if not option_value:
|
||||||
|
LOG.debug("Option '%s' from '%s' section "
|
||||||
|
"is not configured" % (option, section))
|
||||||
|
if helper_method:
|
||||||
|
res = helper_method(*args, **kwargs)
|
||||||
|
if res:
|
||||||
|
value = res["name"] if "network" in option else res.id
|
||||||
|
LOG.debug("Setting value '%s' for option '%s'" % (value, option))
|
||||||
|
self.conf.set(section, option, value)
|
||||||
|
LOG.debug("Option '{opt}' is configured. "
|
||||||
|
"{opt} = {value}".format(opt=option, value=value))
|
||||||
|
else:
|
||||||
|
LOG.debug("Option '{opt}' is already configured "
|
||||||
|
"in Tempest config file. {opt} = {opt_val}"
|
||||||
|
.format(opt=option, opt_val=option_value))
|
||||||
|
|
||||||
|
def _discover_or_create_image(self):
|
||||||
|
if CONF.tempest.img_name_regex:
|
||||||
|
image = self._discover_image()
|
||||||
|
if image:
|
||||||
|
LOG.debug("Using image '%s' (ID = %s) "
|
||||||
|
"for the tests" % (image.name, image.id))
|
||||||
|
return image
|
||||||
|
|
||||||
|
params = {
|
||||||
|
"name": self.generate_random_name(),
|
||||||
|
"disk_format": CONF.tempest.img_disk_format,
|
||||||
|
"container_format": CONF.tempest.img_container_format,
|
||||||
|
"image_location": os.path.join(self.data_dir, self.image_name),
|
||||||
|
"visibility": "public"
|
||||||
|
}
|
||||||
|
LOG.debug("Creating image '%s'" % params["name"])
|
||||||
|
glance_wrapper = glance.wrap(self.clients.glance, self)
|
||||||
|
image = glance_wrapper.create_image(**params)
|
||||||
|
LOG.debug("Image '%s' (ID = %s) has been "
|
||||||
|
"successfully created!" % (image.name, image.id))
|
||||||
|
self._created_images.append(image)
|
||||||
|
|
||||||
|
return image
|
||||||
|
|
||||||
|
def _discover_or_create_flavor(self, flv_ram):
|
||||||
|
novaclient = self.clients.nova()
|
||||||
|
|
||||||
|
LOG.debug("Trying to discover a flavor with the following "
|
||||||
|
"properties: RAM = %dMB, VCPUs = 1, disk = 0GB" % flv_ram)
|
||||||
|
for flavor in novaclient.flavors.list():
|
||||||
|
if (flavor.ram == flv_ram and
|
||||||
|
flavor.vcpus == 1 and flavor.disk == 0):
|
||||||
|
LOG.debug("The following flavor discovered: '{0}'. "
|
||||||
|
"Using flavor '{0}' (ID = {1}) for the tests"
|
||||||
|
.format(flavor.name, flavor.id))
|
||||||
|
return flavor
|
||||||
|
|
||||||
|
LOG.debug("There is no flavor with the mentioned properties")
|
||||||
|
|
||||||
|
params = {
|
||||||
|
"name": self.generate_random_name(),
|
||||||
|
"ram": flv_ram,
|
||||||
|
"vcpus": 1,
|
||||||
|
"disk": 0
|
||||||
|
}
|
||||||
|
LOG.debug("Creating flavor '%s' with the following properties: RAM "
|
||||||
|
"= %dMB, VCPUs = 1, disk = 0GB" % (params["name"], flv_ram))
|
||||||
|
flavor = novaclient.flavors.create(**params)
|
||||||
|
LOG.debug("Flavor '%s' (ID = %s) has been "
|
||||||
|
"successfully created!" % (flavor.name, flavor.id))
|
||||||
|
self._created_flavors.append(flavor)
|
||||||
|
|
||||||
|
return flavor
|
||||||
|
|
||||||
|
def _create_network_resources(self):
|
||||||
|
neutron_wrapper = network.NeutronWrapper(self.clients, self)
|
||||||
|
tenant_id = self.clients.keystone.auth_ref.project_id
|
||||||
|
LOG.debug("Creating network resources: network, subnet, router")
|
||||||
|
net = neutron_wrapper.create_network(
|
||||||
|
tenant_id, subnets_num=1, add_router=True,
|
||||||
|
network_create_args={"shared": True})
|
||||||
|
LOG.debug("Network resources have been successfully created!")
|
||||||
|
self._created_networks.append(net)
|
||||||
|
|
||||||
|
return net
|
||||||
|
|
||||||
|
def _cleanup_tempest_roles(self):
|
||||||
|
keystoneclient = self.clients.keystone()
|
||||||
|
for role in self._created_roles:
|
||||||
|
LOG.debug("Deleting role '%s'" % role.name)
|
||||||
|
keystoneclient.roles.delete(role.id)
|
||||||
|
LOG.debug("Role '%s' has been deleted" % role.name)
|
||||||
|
|
||||||
|
def _cleanup_images(self):
|
||||||
|
glance_wrapper = glance.wrap(self.clients.glance, self)
|
||||||
|
for image in self._created_images:
|
||||||
|
LOG.debug("Deleting image '%s'" % image.name)
|
||||||
|
self.clients.glance().images.delete(image.id)
|
||||||
|
task_utils.wait_for_status(
|
||||||
|
image, ["deleted", "pending_delete"],
|
||||||
|
check_deletion=True,
|
||||||
|
update_resource=glance_wrapper.get_image,
|
||||||
|
timeout=CONF.benchmark.glance_image_delete_timeout,
|
||||||
|
check_interval=CONF.benchmark.
|
||||||
|
glance_image_delete_poll_interval)
|
||||||
|
LOG.debug("Image '%s' has been deleted" % image.name)
|
||||||
|
self._remove_opt_value_from_config("compute", image.id)
|
||||||
|
|
||||||
|
def _cleanup_flavors(self):
|
||||||
|
novaclient = self.clients.nova()
|
||||||
|
for flavor in self._created_flavors:
|
||||||
|
LOG.debug("Deleting flavor '%s'" % flavor.name)
|
||||||
|
novaclient.flavors.delete(flavor.id)
|
||||||
|
LOG.debug("Flavor '%s' has been deleted" % flavor.name)
|
||||||
|
self._remove_opt_value_from_config("compute", flavor.id)
|
||||||
|
self._remove_opt_value_from_config("orchestration", flavor.id)
|
||||||
|
|
||||||
|
def _cleanup_network_resources(self):
|
||||||
|
neutron_wrapper = network.NeutronWrapper(self.clients, self)
|
||||||
|
for net in self._created_networks:
|
||||||
|
LOG.debug("Deleting network resources: router, subnet, network")
|
||||||
|
neutron_wrapper.delete_network(net)
|
||||||
|
self._remove_opt_value_from_config("compute", net["name"])
|
||||||
|
LOG.debug("Network resources have been deleted")
|
||||||
|
|
||||||
|
def _remove_opt_value_from_config(self, section, opt_value):
|
||||||
|
for option, value in self.conf.items(section):
|
||||||
|
if opt_value == value:
|
||||||
|
LOG.debug("Removing value '%s' for option '%s' "
|
||||||
|
"from Tempest config file" % (opt_value, option))
|
||||||
|
self.conf.set(section, option, "")
|
||||||
|
LOG.debug("Value '%s' has been removed" % opt_value)
|
46
rally/plugins/openstack/verification/tempest/consts.py
Normal file
46
rally/plugins/openstack/verification/tempest/consts.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# Copyright 2016: Mirantis 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 rally.common import utils
|
||||||
|
|
||||||
|
|
||||||
|
class _TempestApiTestSets(utils.ImmutableMixin, utils.EnumMixin):
|
||||||
|
BAREMETAL = "baremetal"
|
||||||
|
CLUSTERING = "clustering"
|
||||||
|
COMPUTE = "compute"
|
||||||
|
DATA_PROCESSING = "data_processing"
|
||||||
|
DATABASE = "database"
|
||||||
|
IDENTITY = "identity"
|
||||||
|
IMAGE = "image"
|
||||||
|
MESSAGING = "messaging"
|
||||||
|
NETWORK = "network"
|
||||||
|
OBJECT_STORAGE = "object_storage"
|
||||||
|
ORCHESTRATION = "orchestration"
|
||||||
|
TELEMETRY = "telemetry"
|
||||||
|
VOLUME = "volume"
|
||||||
|
|
||||||
|
|
||||||
|
class _TempestScenarioTestSets(utils.ImmutableMixin, utils.EnumMixin):
|
||||||
|
SCENARIO = "scenario"
|
||||||
|
|
||||||
|
|
||||||
|
class _TempestTestSets(utils.ImmutableMixin, utils.EnumMixin):
|
||||||
|
FULL = "full"
|
||||||
|
SMOKE = "smoke"
|
||||||
|
|
||||||
|
|
||||||
|
TempestApiTestSets = _TempestApiTestSets()
|
||||||
|
TempestScenarioTestSets = _TempestScenarioTestSets()
|
||||||
|
TempestTestSets = _TempestTestSets()
|
178
rally/plugins/openstack/verification/tempest/manager.py
Normal file
178
rally/plugins/openstack/verification/tempest/manager.py
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
# 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 re
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from rally.common.i18n import _LE
|
||||||
|
from rally.common import logging
|
||||||
|
from rally import exceptions
|
||||||
|
from rally.plugins.common.verification import testr
|
||||||
|
from rally.plugins.openstack.verification.tempest import config
|
||||||
|
from rally.plugins.openstack.verification.tempest import consts
|
||||||
|
from rally.verification import manager
|
||||||
|
from rally.verification import utils
|
||||||
|
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@manager.configure(name="tempest", namespace="openstack",
|
||||||
|
default_repo="https://git.openstack.org/openstack/tempest",
|
||||||
|
context={"tempest_configuration": {},
|
||||||
|
"testr_verifier": {}})
|
||||||
|
class TempestManager(testr.TestrLauncher):
|
||||||
|
"""Plugin for Tempest management."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def run_environ(self):
|
||||||
|
env = super(TempestManager, self).run_environ
|
||||||
|
env["TEMPEST_CONFIG_DIR"] = os.path.dirname(self.configfile)
|
||||||
|
env["TEMPEST_CONFIG"] = os.path.basename(self.configfile)
|
||||||
|
# TODO(andreykurilin): move it to Testr base class
|
||||||
|
env["OS_TEST_PATH"] = os.path.join(self.repo_dir,
|
||||||
|
"tempest/test_discover")
|
||||||
|
return env
|
||||||
|
|
||||||
|
@property
|
||||||
|
def configfile(self):
|
||||||
|
return os.path.join(self.home_dir, "tempest.conf")
|
||||||
|
|
||||||
|
def get_configuration(self):
|
||||||
|
return config.read_configfile(self.configfile)
|
||||||
|
|
||||||
|
def configure(self, extra_options=None):
|
||||||
|
cm = config.TempestConfigfileManager(self.verifier.deployment)
|
||||||
|
raw_configfile = cm.create(self.configfile, extra_options)
|
||||||
|
return raw_configfile
|
||||||
|
|
||||||
|
def extend_configuration(self, extra_options):
|
||||||
|
return config.extend_configfile(self.configfile, extra_options)
|
||||||
|
|
||||||
|
def override_configuration(self, new_content):
|
||||||
|
with open(self.configfile, "w") as f:
|
||||||
|
f.write(new_content)
|
||||||
|
|
||||||
|
def install_extension(self, source, version=None, extra=None):
|
||||||
|
"""Install a Tempest plugin."""
|
||||||
|
if extra:
|
||||||
|
raise NotImplementedError(
|
||||||
|
_LE("'%s' verifiers don't support extra options for "
|
||||||
|
"extension installations.")
|
||||||
|
% self.get_name())
|
||||||
|
version = version or "master"
|
||||||
|
egg = re.sub("\.git$", "", os.path.basename(source.strip("/")))
|
||||||
|
full_source = "git+{0}@{1}#egg={2}".format(source, version, egg)
|
||||||
|
# NOTE(ylobankov): Use 'develop mode' installation to provide an
|
||||||
|
# ability to advanced users to change tests or
|
||||||
|
# develop new ones in verifier repo on the fly.
|
||||||
|
cmd = ["pip", "install",
|
||||||
|
"--src", os.path.join(self.base_dir, "extensions"),
|
||||||
|
"-e", full_source]
|
||||||
|
if self.verifier.system_wide:
|
||||||
|
cmd.insert(2, "--no-deps")
|
||||||
|
utils.check_output(cmd, cwd=self.base_dir, env=self.environ)
|
||||||
|
|
||||||
|
# Very often Tempest plugins are inside projects and requirements
|
||||||
|
# for plugins are listed in the test-requirements.txt file.
|
||||||
|
test_reqs_path = os.path.join(self.base_dir, "extensions",
|
||||||
|
egg, "test-requirements.txt")
|
||||||
|
# TODO(andreykurilin): check that packages from test-requirements are
|
||||||
|
# present in system in case of system_wide installation
|
||||||
|
if not self.verifier.system_wide and os.path.exists(test_reqs_path):
|
||||||
|
utils.check_output(["pip", "install", "-r", test_reqs_path],
|
||||||
|
cwd=self.base_dir, env=self.environ)
|
||||||
|
|
||||||
|
def list_extensions(self):
|
||||||
|
"""List all installed Tempest plugins."""
|
||||||
|
# TODO(andreykurilin): find a better way to list tempest plugins
|
||||||
|
cmd = ("from tempest.test_discover import plugins; "
|
||||||
|
"plugins_manager = plugins.TempestTestPluginManager(); "
|
||||||
|
"plugins_map = plugins_manager.get_plugin_load_tests_tuple(); "
|
||||||
|
"plugins_list = ["
|
||||||
|
" {'name': p.name, "
|
||||||
|
" 'entry_point': p.entry_point_target, "
|
||||||
|
" 'location': plugins_map[p.name][1]} "
|
||||||
|
" for p in plugins_manager.ext_plugins.extensions]; "
|
||||||
|
"print(plugins_list)")
|
||||||
|
try:
|
||||||
|
return yaml.load(
|
||||||
|
utils.check_output(["python", "-c", cmd], cwd=self.base_dir,
|
||||||
|
env=self.environ).strip())
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
raise exceptions.RallyException(
|
||||||
|
"Cannot list installed Tempest plugins for verifier %s." %
|
||||||
|
self.verifier)
|
||||||
|
|
||||||
|
def uninstall_extension(self, name):
|
||||||
|
"""Uninstall a Tempest plugin."""
|
||||||
|
for ext in self.list_extensions():
|
||||||
|
if ext["name"] == name and os.path.exists(ext["location"]):
|
||||||
|
shutil.rmtree(ext["location"])
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise exceptions.RallyException(
|
||||||
|
"There is no Tempest plugin with name '%s'. "
|
||||||
|
"Are you sure that it was installed?" % name)
|
||||||
|
|
||||||
|
def list_tests(self, pattern=""):
|
||||||
|
"""List all tests."""
|
||||||
|
if pattern:
|
||||||
|
pattern = self._transform_pattern(pattern)
|
||||||
|
return super(TempestManager, self).list_tests(pattern)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def validate_args(cls, args):
|
||||||
|
"""Validate given arguments."""
|
||||||
|
super(TempestManager, cls).validate_args(args)
|
||||||
|
|
||||||
|
if args.get("pattern"):
|
||||||
|
pattern = args["pattern"].split("=", 1)
|
||||||
|
if len(pattern) == 1:
|
||||||
|
pass # it is just a regex
|
||||||
|
elif pattern[0] == "set":
|
||||||
|
available_sets = (list(consts.TempestTestSets) +
|
||||||
|
list(consts.TempestApiTestSets) +
|
||||||
|
list(consts.TempestScenarioTestSets))
|
||||||
|
if pattern[1] not in available_sets:
|
||||||
|
raise exceptions.ValidationError(
|
||||||
|
"Test set '%s' not found in available "
|
||||||
|
"Tempest test sets. Available sets are '%s'."
|
||||||
|
% (pattern[1], "', '".join(available_sets)))
|
||||||
|
else:
|
||||||
|
raise exceptions.ValidationError(
|
||||||
|
"'pattern' argument should be a regexp or set name "
|
||||||
|
"(format: 'tempest.api.identity.v3', 'set=smoke').")
|
||||||
|
|
||||||
|
def _transform_pattern(self, pattern):
|
||||||
|
"""Transforms tempest-specific pattern to testr format."""
|
||||||
|
parsed_pattern = pattern.split("=", 1)
|
||||||
|
if len(parsed_pattern) == 2:
|
||||||
|
if parsed_pattern[0] == "set":
|
||||||
|
if parsed_pattern[1] in consts.TempestTestSets:
|
||||||
|
return "smoke" if parsed_pattern[1] == "smoke" else ""
|
||||||
|
elif parsed_pattern[1] in consts.TempestApiTestSets:
|
||||||
|
return "tempest.api.%s" % parsed_pattern[1]
|
||||||
|
else:
|
||||||
|
return "tempest.%s" % parsed_pattern[1]
|
||||||
|
return pattern
|
||||||
|
|
||||||
|
def _process_run_args(self, run_args):
|
||||||
|
if run_args.get("pattern"):
|
||||||
|
run_args["pattern"] = self._transform_pattern(run_args["pattern"])
|
||||||
|
return run_args
|
633
tests/unit/plugins/openstack/verification/tempest/test_config.py
Normal file
633
tests/unit/plugins/openstack/verification/tempest/test_config.py
Normal file
@ -0,0 +1,633 @@
|
|||||||
|
# Copyright 2014: Mirantis 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 ddt
|
||||||
|
import mock
|
||||||
|
from oslo_config import cfg
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from rally import exceptions
|
||||||
|
from rally.plugins.openstack.verification.tempest import config
|
||||||
|
from tests.unit import fakes
|
||||||
|
from tests.unit import test
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
|
||||||
|
CREDS = {
|
||||||
|
"admin": {
|
||||||
|
"username": "admin",
|
||||||
|
"tenant_name": "admin",
|
||||||
|
"password": "admin-12345",
|
||||||
|
"auth_url": "http://test:5000/v2.0/",
|
||||||
|
"permission": "admin",
|
||||||
|
"region_name": "test",
|
||||||
|
"https_insecure": False,
|
||||||
|
"https_cacert": "/path/to/cacert/file",
|
||||||
|
"user_domain_name": "admin",
|
||||||
|
"project_domain_name": "admin"
|
||||||
|
},
|
||||||
|
"uuid": "fake_deployment"
|
||||||
|
}
|
||||||
|
|
||||||
|
PATH = "rally.plugins.openstack.verification.tempest.config"
|
||||||
|
|
||||||
|
|
||||||
|
@ddt.ddt
|
||||||
|
class TempestConfigTestCase(test.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TempestConfigTestCase, self).setUp()
|
||||||
|
|
||||||
|
mock.patch("rally.osclients.Clients").start()
|
||||||
|
|
||||||
|
self.tempest_conf = config.TempestConfigfileManager(CREDS)
|
||||||
|
|
||||||
|
@ddt.data({"publicURL": "test_url"},
|
||||||
|
{"interface": "public", "url": "test_url"})
|
||||||
|
def test__get_service_url(self, endpoint):
|
||||||
|
mock_catalog = mock.MagicMock()
|
||||||
|
mock_catalog.get_endpoints.return_value = {
|
||||||
|
"test_service_type": [endpoint]}
|
||||||
|
|
||||||
|
self.tempest_conf.keystone.service_catalog = mock_catalog
|
||||||
|
self.tempest_conf.clients.services.return_value = {
|
||||||
|
"test_service_type": "test_service"}
|
||||||
|
self.assertEqual(
|
||||||
|
self.tempest_conf._get_service_url("test_service"), "test_url")
|
||||||
|
|
||||||
|
def test__configure_auth(self):
|
||||||
|
self.tempest_conf._configure_auth()
|
||||||
|
|
||||||
|
expected = (
|
||||||
|
("admin_username", CREDS["admin"]["username"]),
|
||||||
|
("admin_password", CREDS["admin"]["password"]),
|
||||||
|
("admin_project_name", CREDS["admin"]["tenant_name"]),
|
||||||
|
("admin_domain_name", CREDS["admin"]["user_domain_name"]))
|
||||||
|
result = self.tempest_conf.conf.items("auth")
|
||||||
|
for item in expected:
|
||||||
|
self.assertIn(item, result)
|
||||||
|
|
||||||
|
@ddt.data("data_processing", "data-processing")
|
||||||
|
def test__configure_data_processing(self, service_type):
|
||||||
|
self.tempest_conf.available_services = ["sahara"]
|
||||||
|
|
||||||
|
self.tempest_conf.clients.services.return_value = {
|
||||||
|
service_type: "sahara"}
|
||||||
|
self.tempest_conf._configure_data_processing()
|
||||||
|
self.assertEqual(
|
||||||
|
self.tempest_conf.conf.get(
|
||||||
|
"data-processing", "catalog_type"), service_type)
|
||||||
|
|
||||||
|
def test__configure_identity(self):
|
||||||
|
self.tempest_conf._configure_identity()
|
||||||
|
|
||||||
|
expected = (
|
||||||
|
("region", CREDS["admin"]["region_name"]),
|
||||||
|
("auth_version", "v2"),
|
||||||
|
("uri", CREDS["admin"]["auth_url"][:-1]),
|
||||||
|
("uri_v3", CREDS["admin"]["auth_url"].replace("/v2.0/", "/v3")),
|
||||||
|
("disable_ssl_certificate_validation",
|
||||||
|
str(CREDS["admin"]["https_insecure"])),
|
||||||
|
("ca_certificates_file", CREDS["admin"]["https_cacert"]))
|
||||||
|
result = self.tempest_conf.conf.items("identity")
|
||||||
|
for item in expected:
|
||||||
|
self.assertIn(item, result)
|
||||||
|
|
||||||
|
def test__configure_network_if_neutron(self):
|
||||||
|
self.tempest_conf.available_services = ["neutron"]
|
||||||
|
client = self.tempest_conf.clients.neutron()
|
||||||
|
client.list_networks.return_value = {
|
||||||
|
"networks": [
|
||||||
|
{
|
||||||
|
"status": "ACTIVE",
|
||||||
|
"id": "test_id",
|
||||||
|
"router:external": True
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
self.tempest_conf._configure_network()
|
||||||
|
self.assertEqual(
|
||||||
|
self.tempest_conf.conf.get("network",
|
||||||
|
"public_network_id"), "test_id")
|
||||||
|
|
||||||
|
def test__configure_network_if_nova(self):
|
||||||
|
self.tempest_conf.available_services = ["nova"]
|
||||||
|
client = self.tempest_conf.clients.nova()
|
||||||
|
client.networks.list.return_value = [
|
||||||
|
mock.MagicMock(human_id="fake-network")]
|
||||||
|
|
||||||
|
self.tempest_conf._configure_network()
|
||||||
|
|
||||||
|
expected = {"compute": ("fixed_network_name", "fake-network"),
|
||||||
|
"validation": ("network_for_ssh", "fake-network")}
|
||||||
|
for section, option in expected.items():
|
||||||
|
result = self.tempest_conf.conf.items(section)
|
||||||
|
self.assertIn(option, result)
|
||||||
|
|
||||||
|
def test__configure_network_feature_enabled(self):
|
||||||
|
self.tempest_conf.available_services = ["neutron"]
|
||||||
|
client = self.tempest_conf.clients.neutron()
|
||||||
|
client.list_ext.return_value = {
|
||||||
|
"extensions": [
|
||||||
|
{"alias": "dvr"},
|
||||||
|
{"alias": "extra_dhcp_opt"},
|
||||||
|
{"alias": "extraroute"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
self.tempest_conf._configure_network_feature_enabled()
|
||||||
|
client.list_ext.assert_called_once_with("extensions", "/extensions",
|
||||||
|
retrieve_all=True)
|
||||||
|
self.assertEqual(self.tempest_conf.conf.get(
|
||||||
|
"network-feature-enabled", "api_extensions"),
|
||||||
|
"dvr,extra_dhcp_opt,extraroute")
|
||||||
|
|
||||||
|
@mock.patch("os.makedirs")
|
||||||
|
@mock.patch("os.path.exists", return_value=False)
|
||||||
|
def test__configure_oslo_concurrency(self, mock_exists, mock_makedirs):
|
||||||
|
self.tempest_conf._configure_oslo_concurrency()
|
||||||
|
|
||||||
|
lock_path = os.path.join(
|
||||||
|
self.tempest_conf.data_dir, "lock_files_fake_deployment")
|
||||||
|
mock_makedirs.assert_called_with(lock_path)
|
||||||
|
self.assertEqual(
|
||||||
|
self.tempest_conf.conf.get(
|
||||||
|
"oslo_concurrency", "lock_path"), lock_path)
|
||||||
|
|
||||||
|
def test__configure_object_storage(self):
|
||||||
|
self.tempest_conf._configure_object_storage()
|
||||||
|
|
||||||
|
expected = (
|
||||||
|
("operator_role", CONF.tempest.swift_operator_role),
|
||||||
|
("reseller_admin_role", CONF.tempest.swift_reseller_admin_role))
|
||||||
|
result = self.tempest_conf.conf.items("object-storage")
|
||||||
|
for item in expected:
|
||||||
|
self.assertIn(item, result)
|
||||||
|
|
||||||
|
def test__configure_orchestration(self):
|
||||||
|
self.tempest_conf._configure_orchestration()
|
||||||
|
|
||||||
|
expected = (
|
||||||
|
("stack_owner_role", CONF.tempest.heat_stack_owner_role),
|
||||||
|
("stack_user_role", CONF.tempest.heat_stack_user_role))
|
||||||
|
result = self.tempest_conf.conf.items("orchestration")
|
||||||
|
for item in expected:
|
||||||
|
self.assertIn(item, result)
|
||||||
|
|
||||||
|
def test__configure_scenario(self):
|
||||||
|
self.tempest_conf._configure_scenario()
|
||||||
|
|
||||||
|
expected = (("img_dir", self.tempest_conf.data_dir),)
|
||||||
|
result = self.tempest_conf.conf.items("scenario")
|
||||||
|
for item in expected:
|
||||||
|
self.assertIn(item, result)
|
||||||
|
|
||||||
|
def test__configure_service_available(self):
|
||||||
|
available_services = ("nova", "cinder", "glance", "sahara")
|
||||||
|
self.tempest_conf.available_services = available_services
|
||||||
|
self.tempest_conf._configure_service_available()
|
||||||
|
|
||||||
|
expected = (
|
||||||
|
("neutron", "False"), ("heat", "False"), ("nova", "True"),
|
||||||
|
("swift", "False"), ("cinder", "True"), ("sahara", "True"),
|
||||||
|
("glance", "True"))
|
||||||
|
result = self.tempest_conf.conf.items("service_available")
|
||||||
|
for item in expected:
|
||||||
|
self.assertIn(item, result)
|
||||||
|
|
||||||
|
@ddt.data({}, {"service": "neutron", "connect_method": "floating"})
|
||||||
|
@ddt.unpack
|
||||||
|
def test__configure_validation(self, service="nova",
|
||||||
|
connect_method="fixed"):
|
||||||
|
self.tempest_conf.available_services = [service]
|
||||||
|
self.tempest_conf._configure_validation()
|
||||||
|
|
||||||
|
expected = (("run_validation", "True"),
|
||||||
|
("connect_method", connect_method))
|
||||||
|
result = self.tempest_conf.conf.items("validation")
|
||||||
|
for item in expected:
|
||||||
|
self.assertIn(item, result)
|
||||||
|
|
||||||
|
@mock.patch("rally.plugins.openstack.verification."
|
||||||
|
"tempest.config.read_configfile")
|
||||||
|
@mock.patch("rally.plugins.openstack.verification."
|
||||||
|
"tempest.config.write_configfile")
|
||||||
|
@mock.patch("inspect.getmembers")
|
||||||
|
def test_create(self, mock_inspect_getmembers, mock_write_configfile,
|
||||||
|
mock_read_configfile):
|
||||||
|
configure_something_method = mock.MagicMock()
|
||||||
|
mock_inspect_getmembers.return_value = [("_configure_something",
|
||||||
|
configure_something_method)]
|
||||||
|
|
||||||
|
fake_extra_conf = {"section": {"option": "value"}}
|
||||||
|
|
||||||
|
self.assertEqual(mock_read_configfile.return_value,
|
||||||
|
self.tempest_conf.create("/path/to/fake/conf",
|
||||||
|
fake_extra_conf))
|
||||||
|
self.assertEqual(configure_something_method.call_count, 1)
|
||||||
|
self.assertIn(("option", "value"),
|
||||||
|
self.tempest_conf.conf.items("section"))
|
||||||
|
self.assertEqual(mock_write_configfile.call_count, 1)
|
||||||
|
|
||||||
|
|
||||||
|
@ddt.ddt
|
||||||
|
class TempestResourcesContextTestCase(test.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TempestResourcesContextTestCase, self).setUp()
|
||||||
|
|
||||||
|
mock.patch("rally.osclients.Clients").start()
|
||||||
|
self.mock_isfile = mock.patch("os.path.isfile",
|
||||||
|
return_value=True).start()
|
||||||
|
|
||||||
|
cfg = {"verifier": mock.Mock(deployment=CREDS),
|
||||||
|
"verification": {"uuid": "uuid"}}
|
||||||
|
cfg["verifier"].manager.configfile = "/fake/path/to/config"
|
||||||
|
self.context = config.TempestResourcesContext(cfg)
|
||||||
|
self.context.conf.add_section("compute")
|
||||||
|
self.context.conf.add_section("orchestration")
|
||||||
|
self.context.conf.add_section("scenario")
|
||||||
|
|
||||||
|
@mock.patch("six.moves.builtins.open", side_effect=mock.mock_open(),
|
||||||
|
create=True)
|
||||||
|
def test__download_image_from_glance(self, mock_open):
|
||||||
|
self.mock_isfile.return_value = False
|
||||||
|
img_path = os.path.join(self.context.data_dir, "foo")
|
||||||
|
img = mock.MagicMock()
|
||||||
|
img.data.return_value = "data"
|
||||||
|
|
||||||
|
config._download_image(img_path, img)
|
||||||
|
mock_open.assert_called_once_with(img_path, "wb")
|
||||||
|
mock_open().write.assert_has_calls([mock.call("d"),
|
||||||
|
mock.call("a"),
|
||||||
|
mock.call("t"),
|
||||||
|
mock.call("a")])
|
||||||
|
|
||||||
|
@mock.patch("six.moves.builtins.open", side_effect=mock.mock_open())
|
||||||
|
@mock.patch("requests.get", return_value=mock.MagicMock(status_code=200))
|
||||||
|
def test__download_image_from_url_success(self, mock_get, mock_open):
|
||||||
|
self.mock_isfile.return_value = False
|
||||||
|
img_path = os.path.join(self.context.data_dir, "foo")
|
||||||
|
mock_get.return_value.iter_content.return_value = "data"
|
||||||
|
|
||||||
|
config._download_image(img_path)
|
||||||
|
mock_get.assert_called_once_with(CONF.tempest.img_url, stream=True)
|
||||||
|
mock_open.assert_called_once_with(img_path, "wb")
|
||||||
|
mock_open().write.assert_has_calls([mock.call("d"),
|
||||||
|
mock.call("a"),
|
||||||
|
mock.call("t"),
|
||||||
|
mock.call("a")])
|
||||||
|
|
||||||
|
@mock.patch("requests.get")
|
||||||
|
@ddt.data(404, 500)
|
||||||
|
def test__download_image_from_url_failure(self, status_code, mock_get):
|
||||||
|
self.mock_isfile.return_value = False
|
||||||
|
mock_get.return_value = mock.MagicMock(status_code=status_code)
|
||||||
|
self.assertRaises(
|
||||||
|
exceptions.TempestConfigCreationFailure, config._download_image,
|
||||||
|
os.path.join(self.context.data_dir, "foo"))
|
||||||
|
|
||||||
|
@mock.patch("requests.get", side_effect=requests.ConnectionError())
|
||||||
|
def test__download_image_from_url_connection_error(
|
||||||
|
self, mock_requests_get):
|
||||||
|
self.mock_isfile.return_value = False
|
||||||
|
self.assertRaises(
|
||||||
|
exceptions.TempestConfigCreationFailure, config._download_image,
|
||||||
|
os.path.join(self.context.data_dir, "foo"))
|
||||||
|
|
||||||
|
@mock.patch("rally.plugins.openstack.wrappers."
|
||||||
|
"network.NeutronWrapper.create_network")
|
||||||
|
@mock.patch("six.moves.builtins.open", side_effect=mock.mock_open())
|
||||||
|
def test_options_configured_manually(
|
||||||
|
self, mock_open, mock_neutron_wrapper_create_network):
|
||||||
|
self.context.available_services = ["glance", "heat", "nova", "neutron"]
|
||||||
|
|
||||||
|
self.context.conf.set("compute", "image_ref", "id1")
|
||||||
|
self.context.conf.set("compute", "image_ref_alt", "id2")
|
||||||
|
self.context.conf.set("compute", "flavor_ref", "id3")
|
||||||
|
self.context.conf.set("compute", "flavor_ref_alt", "id4")
|
||||||
|
self.context.conf.set("compute", "fixed_network_name", "name1")
|
||||||
|
self.context.conf.set("orchestration", "instance_type", "id5")
|
||||||
|
self.context.conf.set("scenario", "img_file", "id6")
|
||||||
|
|
||||||
|
self.context.__enter__()
|
||||||
|
|
||||||
|
glanceclient = self.context.clients.glance()
|
||||||
|
novaclient = self.context.clients.nova()
|
||||||
|
|
||||||
|
self.assertEqual(glanceclient.images.create.call_count, 0)
|
||||||
|
self.assertEqual(novaclient.flavors.create.call_count, 0)
|
||||||
|
self.assertEqual(mock_neutron_wrapper_create_network.call_count, 0)
|
||||||
|
|
||||||
|
def test__create_tempest_roles(self):
|
||||||
|
role1 = CONF.tempest.swift_operator_role
|
||||||
|
role2 = CONF.tempest.swift_reseller_admin_role
|
||||||
|
role3 = CONF.tempest.heat_stack_owner_role
|
||||||
|
role4 = CONF.tempest.heat_stack_user_role
|
||||||
|
|
||||||
|
client = self.context.clients.verified_keystone()
|
||||||
|
client.roles.list.return_value = [fakes.FakeRole(name=role1),
|
||||||
|
fakes.FakeRole(name=role2)]
|
||||||
|
client.roles.create.side_effect = [fakes.FakeFlavor(name=role3),
|
||||||
|
fakes.FakeFlavor(name=role4)]
|
||||||
|
|
||||||
|
self.context._create_tempest_roles()
|
||||||
|
self.assertEqual(client.roles.create.call_count, 2)
|
||||||
|
|
||||||
|
created_roles = [role.name for role in self.context._created_roles]
|
||||||
|
self.assertIn(role3, created_roles)
|
||||||
|
self.assertIn(role4, created_roles)
|
||||||
|
|
||||||
|
@mock.patch("rally.plugins.openstack.wrappers.glance.wrap")
|
||||||
|
def test__discover_image(self, mock_wrap):
|
||||||
|
client = mock_wrap.return_value
|
||||||
|
client.list_images.return_value = [fakes.FakeImage(name="Foo"),
|
||||||
|
fakes.FakeImage(name="CirrOS")]
|
||||||
|
|
||||||
|
image = self.context._discover_image()
|
||||||
|
self.assertEqual("CirrOS", image.name)
|
||||||
|
|
||||||
|
@mock.patch("six.moves.builtins.open", side_effect=mock.mock_open(),
|
||||||
|
create=True)
|
||||||
|
@mock.patch("rally.plugins.openstack.wrappers.glance.wrap")
|
||||||
|
@mock.patch("os.path.isfile", return_value=False)
|
||||||
|
def test__download_image(self, mock_isfile, mock_wrap, mock_open):
|
||||||
|
img_1 = mock.MagicMock()
|
||||||
|
img_1.name = "Foo"
|
||||||
|
img_2 = mock.MagicMock()
|
||||||
|
img_2.name = "CirrOS"
|
||||||
|
img_2.data.return_value = "data"
|
||||||
|
mock_wrap.return_value.list_images.return_value = [img_1, img_2]
|
||||||
|
|
||||||
|
self.context._download_image()
|
||||||
|
img_path = os.path.join(self.context.data_dir, self.context.image_name)
|
||||||
|
mock_open.assert_called_once_with(img_path, "wb")
|
||||||
|
mock_open().write.assert_has_calls([mock.call("d"),
|
||||||
|
mock.call("a"),
|
||||||
|
mock.call("t"),
|
||||||
|
mock.call("a")])
|
||||||
|
|
||||||
|
# We can choose any option to test the '_configure_option' method. So let's
|
||||||
|
# configure the 'flavor_ref' option.
|
||||||
|
def test__configure_option(self):
|
||||||
|
helper_method = mock.MagicMock()
|
||||||
|
helper_method.side_effect = [fakes.FakeFlavor(id="id1")]
|
||||||
|
|
||||||
|
self.context.conf.set("compute", "flavor_ref", "")
|
||||||
|
self.context._configure_option("compute", "flavor_ref",
|
||||||
|
helper_method=helper_method, flv_ram=64)
|
||||||
|
self.assertEqual(helper_method.call_count, 1)
|
||||||
|
|
||||||
|
result = self.context.conf.get("compute", "flavor_ref")
|
||||||
|
self.assertEqual("id1", result)
|
||||||
|
|
||||||
|
@mock.patch("rally.plugins.openstack.wrappers.glance.wrap")
|
||||||
|
def test__discover_or_create_image_when_image_exists(self, mock_wrap):
|
||||||
|
client = mock_wrap.return_value
|
||||||
|
client.list_images.return_value = [fakes.FakeImage(name="CirrOS")]
|
||||||
|
|
||||||
|
image = self.context._discover_or_create_image()
|
||||||
|
self.assertEqual("CirrOS", image.name)
|
||||||
|
self.assertEqual(0, client.create_image.call_count)
|
||||||
|
self.assertEqual(0, len(self.context._created_images))
|
||||||
|
|
||||||
|
@mock.patch("rally.plugins.openstack.wrappers.glance.wrap")
|
||||||
|
def test__discover_or_create_image(self, mock_wrap):
|
||||||
|
client = mock_wrap.return_value
|
||||||
|
|
||||||
|
image = self.context._discover_or_create_image()
|
||||||
|
self.assertEqual(image, client.create_image.return_value)
|
||||||
|
self.assertEqual(self.context._created_images[0],
|
||||||
|
client.create_image.return_value)
|
||||||
|
client.create_image.assert_called_once_with(
|
||||||
|
container_format=CONF.tempest.img_container_format,
|
||||||
|
image_location=mock.ANY,
|
||||||
|
disk_format=CONF.tempest.img_disk_format,
|
||||||
|
name=mock.ANY,
|
||||||
|
visibility="public")
|
||||||
|
|
||||||
|
def test__discover_or_create_flavor_when_flavor_exists(self):
|
||||||
|
client = self.context.clients.nova()
|
||||||
|
client.flavors.list.return_value = [fakes.FakeFlavor(id="id1", ram=64,
|
||||||
|
vcpus=1, disk=0)]
|
||||||
|
|
||||||
|
flavor = self.context._discover_or_create_flavor(64)
|
||||||
|
self.assertEqual("id1", flavor.id)
|
||||||
|
self.assertEqual(0, len(self.context._created_flavors))
|
||||||
|
|
||||||
|
def test__discover_or_create_flavor(self):
|
||||||
|
client = self.context.clients.nova()
|
||||||
|
client.flavors.create.side_effect = [fakes.FakeFlavor(id="id1")]
|
||||||
|
|
||||||
|
flavor = self.context._discover_or_create_flavor(64)
|
||||||
|
self.assertEqual("id1", flavor.id)
|
||||||
|
self.assertEqual("id1", self.context._created_flavors[0].id)
|
||||||
|
|
||||||
|
def test__create_network_resources(self):
|
||||||
|
client = self.context.clients.neutron()
|
||||||
|
fake_network = {
|
||||||
|
"id": "nid1",
|
||||||
|
"name": "network",
|
||||||
|
"status": "status"}
|
||||||
|
|
||||||
|
client.create_network.side_effect = [{"network": fake_network}]
|
||||||
|
client.create_router.side_effect = [{"router": {"id": "rid1"}}]
|
||||||
|
client.create_subnet.side_effect = [{"subnet": {"id": "subid1"}}]
|
||||||
|
|
||||||
|
network = self.context._create_network_resources()
|
||||||
|
self.assertEqual("nid1", network["id"])
|
||||||
|
self.assertEqual("nid1", self.context._created_networks[0]["id"])
|
||||||
|
self.assertEqual("rid1",
|
||||||
|
self.context._created_networks[0]["router_id"])
|
||||||
|
self.assertEqual("subid1",
|
||||||
|
self.context._created_networks[0]["subnets"][0])
|
||||||
|
|
||||||
|
def test__cleanup_tempest_roles(self):
|
||||||
|
self.context._created_roles = [fakes.FakeRole(), fakes.FakeRole()]
|
||||||
|
|
||||||
|
self.context._cleanup_tempest_roles()
|
||||||
|
client = self.context.clients.keystone()
|
||||||
|
self.assertEqual(client.roles.delete.call_count, 2)
|
||||||
|
|
||||||
|
@mock.patch("rally.plugins.openstack.wrappers.glance.wrap")
|
||||||
|
def test__cleanup_images(self, mock_wrap):
|
||||||
|
self.context._created_images = [fakes.FakeImage(id="id1"),
|
||||||
|
fakes.FakeImage(id="id2")]
|
||||||
|
|
||||||
|
self.context.conf.set("compute", "image_ref", "id1")
|
||||||
|
self.context.conf.set("compute", "image_ref_alt", "id2")
|
||||||
|
|
||||||
|
wrapper = mock_wrap.return_value
|
||||||
|
wrapper.get_image.side_effect = [
|
||||||
|
fakes.FakeImage(id="id1", status="DELETED"),
|
||||||
|
fakes.FakeImage(id="id2"),
|
||||||
|
fakes.FakeImage(id="id2", status="DELETED")]
|
||||||
|
|
||||||
|
self.context._cleanup_images()
|
||||||
|
client = self.context.clients.glance()
|
||||||
|
client.images.delete.assert_has_calls([mock.call("id1"),
|
||||||
|
mock.call("id2")])
|
||||||
|
|
||||||
|
self.assertEqual("", self.context.conf.get("compute", "image_ref"))
|
||||||
|
self.assertEqual("", self.context.conf.get("compute", "image_ref_alt"))
|
||||||
|
|
||||||
|
def test__cleanup_flavors(self):
|
||||||
|
self.context._created_flavors = [fakes.FakeFlavor(id="id1"),
|
||||||
|
fakes.FakeFlavor(id="id2"),
|
||||||
|
fakes.FakeFlavor(id="id3")]
|
||||||
|
|
||||||
|
self.context.conf.set("compute", "flavor_ref", "id1")
|
||||||
|
self.context.conf.set("compute", "flavor_ref_alt", "id2")
|
||||||
|
self.context.conf.set("orchestration", "instance_type", "id3")
|
||||||
|
|
||||||
|
self.context._cleanup_flavors()
|
||||||
|
client = self.context.clients.nova()
|
||||||
|
self.assertEqual(client.flavors.delete.call_count, 3)
|
||||||
|
|
||||||
|
self.assertEqual("", self.context.conf.get("compute", "flavor_ref"))
|
||||||
|
self.assertEqual("", self.context.conf.get("compute",
|
||||||
|
"flavor_ref_alt"))
|
||||||
|
self.assertEqual("", self.context.conf.get("orchestration",
|
||||||
|
"instance_type"))
|
||||||
|
|
||||||
|
@mock.patch("rally.plugins.openstack.wrappers."
|
||||||
|
"network.NeutronWrapper.delete_network")
|
||||||
|
def test__cleanup_network_resources(
|
||||||
|
self, mock_neutron_wrapper_delete_network):
|
||||||
|
self.context._created_networks = [{"name": "net-12345"}]
|
||||||
|
self.context.conf.set("compute", "fixed_network_name", "net-12345")
|
||||||
|
|
||||||
|
self.context._cleanup_network_resources()
|
||||||
|
self.assertEqual(mock_neutron_wrapper_delete_network.call_count, 1)
|
||||||
|
self.assertEqual("", self.context.conf.get("compute",
|
||||||
|
"fixed_network_name"))
|
||||||
|
|
||||||
|
@mock.patch("%s.write_configfile" % PATH)
|
||||||
|
@mock.patch("%s.TempestResourcesContext._configure_option" % PATH)
|
||||||
|
@mock.patch("%s.TempestResourcesContext._create_tempest_roles" % PATH)
|
||||||
|
@mock.patch("%s._create_or_get_data_dir" % PATH)
|
||||||
|
@mock.patch("%s.osclients.Clients" % PATH)
|
||||||
|
def test_setup(self, mock_clients, mock__create_or_get_data_dir,
|
||||||
|
mock__create_tempest_roles, mock__configure_option,
|
||||||
|
mock_write_configfile):
|
||||||
|
mock_clients.return_value.services.return_value = {}
|
||||||
|
verifier = mock.MagicMock(deployment=CREDS)
|
||||||
|
cfg = config.TempestResourcesContext({"verifier": verifier})
|
||||||
|
cfg.conf = mock.Mock()
|
||||||
|
|
||||||
|
# case #1: no neutron and heat
|
||||||
|
cfg.setup()
|
||||||
|
|
||||||
|
cfg.conf.read.assert_called_once_with(verifier.manager.configfile)
|
||||||
|
mock__create_or_get_data_dir.assert_called_once_with()
|
||||||
|
mock__create_tempest_roles.assert_called_once_with()
|
||||||
|
mock_write_configfile.assert_called_once_with(
|
||||||
|
verifier.manager.configfile, cfg.conf)
|
||||||
|
self.assertEqual(
|
||||||
|
[mock.call("scenario", "img_file", cfg.image_name,
|
||||||
|
helper_method=cfg._download_image),
|
||||||
|
mock.call("compute", "image_ref",
|
||||||
|
helper_method=cfg._discover_or_create_image),
|
||||||
|
mock.call("compute", "image_ref_alt",
|
||||||
|
helper_method=cfg._discover_or_create_image),
|
||||||
|
mock.call("compute", "flavor_ref",
|
||||||
|
helper_method=cfg._discover_or_create_flavor,
|
||||||
|
flv_ram=config.CONF.tempest.flavor_ref_ram),
|
||||||
|
mock.call("compute", "flavor_ref_alt",
|
||||||
|
helper_method=cfg._discover_or_create_flavor,
|
||||||
|
flv_ram=config.CONF.tempest.flavor_ref_alt_ram)],
|
||||||
|
mock__configure_option.call_args_list)
|
||||||
|
|
||||||
|
cfg.conf.reset_mock()
|
||||||
|
mock__create_or_get_data_dir.reset_mock()
|
||||||
|
mock__create_tempest_roles.reset_mock()
|
||||||
|
mock_write_configfile.reset_mock()
|
||||||
|
mock__configure_option.reset_mock()
|
||||||
|
|
||||||
|
# case #2: neutron and heat are presented
|
||||||
|
mock_clients.return_value.services.return_value = {
|
||||||
|
"network": "neutron", "orchestration": "heat"}
|
||||||
|
cfg.setup()
|
||||||
|
|
||||||
|
cfg.conf.read.assert_called_once_with(verifier.manager.configfile)
|
||||||
|
mock__create_or_get_data_dir.assert_called_once_with()
|
||||||
|
mock__create_tempest_roles.assert_called_once_with()
|
||||||
|
mock_write_configfile.assert_called_once_with(
|
||||||
|
verifier.manager.configfile, cfg.conf)
|
||||||
|
self.assertEqual(
|
||||||
|
[mock.call("scenario", "img_file", cfg.image_name,
|
||||||
|
helper_method=cfg._download_image),
|
||||||
|
mock.call("compute", "image_ref",
|
||||||
|
helper_method=cfg._discover_or_create_image),
|
||||||
|
mock.call("compute", "image_ref_alt",
|
||||||
|
helper_method=cfg._discover_or_create_image),
|
||||||
|
mock.call("compute", "flavor_ref",
|
||||||
|
helper_method=cfg._discover_or_create_flavor,
|
||||||
|
flv_ram=config.CONF.tempest.flavor_ref_ram),
|
||||||
|
mock.call("compute", "flavor_ref_alt",
|
||||||
|
helper_method=cfg._discover_or_create_flavor,
|
||||||
|
flv_ram=config.CONF.tempest.flavor_ref_alt_ram),
|
||||||
|
mock.call("compute", "fixed_network_name",
|
||||||
|
helper_method=cfg._create_network_resources),
|
||||||
|
mock.call("orchestration", "instance_type",
|
||||||
|
helper_method=cfg._discover_or_create_flavor,
|
||||||
|
flv_ram=config.CONF.tempest.heat_instance_type_ram)],
|
||||||
|
mock__configure_option.call_args_list)
|
||||||
|
|
||||||
|
|
||||||
|
class UtilsTestCase(test.TestCase):
|
||||||
|
|
||||||
|
@mock.patch("six.moves.builtins.open", side_effect=mock.mock_open())
|
||||||
|
def test_write_configfile(self, mock_open):
|
||||||
|
conf_path = "/path/to/fake/conf"
|
||||||
|
conf_data = mock.Mock()
|
||||||
|
|
||||||
|
config.write_configfile(conf_path, conf_data)
|
||||||
|
mock_open.assert_called_once_with(conf_path, "w")
|
||||||
|
conf_data.write.assert_called_once_with(mock_open.side_effect())
|
||||||
|
|
||||||
|
@mock.patch("six.moves.builtins.open", side_effect=mock.mock_open())
|
||||||
|
def test_read_configfile(self, mock_open):
|
||||||
|
conf_path = "/path/to/fake/conf"
|
||||||
|
|
||||||
|
config.read_configfile(conf_path)
|
||||||
|
mock_open.assert_called_once_with(conf_path)
|
||||||
|
mock_open.side_effect().read.assert_called_once_with()
|
||||||
|
|
||||||
|
@mock.patch("rally.plugins.openstack.verification.tempest.config."
|
||||||
|
"six.StringIO")
|
||||||
|
@mock.patch("rally.plugins.openstack.verification.tempest.config."
|
||||||
|
"write_configfile")
|
||||||
|
@mock.patch("rally.plugins.openstack.verification.tempest.config."
|
||||||
|
"add_extra_options")
|
||||||
|
@mock.patch("rally.plugins.openstack.verification.tempest.config."
|
||||||
|
"configparser")
|
||||||
|
def test_extend_configfile(self, mock_configparser, mock_add_extra_options,
|
||||||
|
mock_write_configfile, mock_string_io):
|
||||||
|
conf_path = "/path/to/fake/conf"
|
||||||
|
extra_options = mock.Mock()
|
||||||
|
|
||||||
|
config.extend_configfile(conf_path, extra_options)
|
||||||
|
|
||||||
|
mock_configparser.ConfigParser.assert_called_once_with()
|
||||||
|
conf = mock_configparser.ConfigParser.return_value
|
||||||
|
conf.read.assert_called_once_with(conf_path)
|
||||||
|
mock_add_extra_options.assert_called_once_with(extra_options, conf)
|
||||||
|
conf.write.assert_called_once_with(mock_string_io.return_value)
|
||||||
|
mock_string_io.return_value.getvalue.assert_called_once_with()
|
@ -0,0 +1,233 @@
|
|||||||
|
#
|
||||||
|
# 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 os
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
import mock
|
||||||
|
|
||||||
|
from rally import exceptions
|
||||||
|
from rally.plugins.openstack.verification.tempest import manager
|
||||||
|
from tests.unit import test
|
||||||
|
|
||||||
|
|
||||||
|
PATH = "rally.plugins.openstack.verification.tempest.manager"
|
||||||
|
|
||||||
|
|
||||||
|
class TempestManagerTestCase(test.TestCase):
|
||||||
|
def test_run_environ_property(self):
|
||||||
|
mock.patch("%s.testr.TestrLauncher.run_environ" % PATH,
|
||||||
|
new={"some": "key"}).start()
|
||||||
|
tempest = manager.TempestManager(mock.MagicMock(uuid="uuuiiiddd"))
|
||||||
|
env = {"some": "key",
|
||||||
|
"OS_TEST_PATH": os.path.join(tempest.repo_dir,
|
||||||
|
"tempest/test_discover"),
|
||||||
|
"TEMPEST_CONFIG": "tempest.conf",
|
||||||
|
"TEMPEST_CONFIG_DIR": os.path.dirname(tempest.configfile)}
|
||||||
|
|
||||||
|
self.assertEqual(env, tempest.run_environ)
|
||||||
|
|
||||||
|
def test_configfile_property(self):
|
||||||
|
tempest = manager.TempestManager(mock.MagicMock(uuid="uuuiiiddd"))
|
||||||
|
self.assertEqual(os.path.join(tempest.home_dir, "tempest.conf"),
|
||||||
|
tempest.configfile)
|
||||||
|
|
||||||
|
@mock.patch("%s.config.read_configfile" % PATH)
|
||||||
|
def test_get_configuration(self, mock_read_configfile):
|
||||||
|
tempest = manager.TempestManager(mock.MagicMock(uuid="uuuiiiddd"))
|
||||||
|
self.assertEqual(mock_read_configfile.return_value,
|
||||||
|
tempest.get_configuration())
|
||||||
|
mock_read_configfile.assert_called_once_with(tempest.configfile)
|
||||||
|
|
||||||
|
@mock.patch("%s.config.TempestConfigfileManager" % PATH)
|
||||||
|
def test_configure(self, mock_tempest_configfile_manager):
|
||||||
|
tempest = manager.TempestManager(mock.MagicMock(uuid="uuuiiiddd"))
|
||||||
|
cm = mock_tempest_configfile_manager.return_value
|
||||||
|
extra_options = mock.Mock()
|
||||||
|
|
||||||
|
self.assertEqual(cm.create.return_value,
|
||||||
|
tempest.configure(extra_options))
|
||||||
|
mock_tempest_configfile_manager.assert_called_once_with(
|
||||||
|
tempest.verifier.deployment)
|
||||||
|
cm.create.assert_called_once_with(tempest.configfile, extra_options)
|
||||||
|
|
||||||
|
@mock.patch("%s.config.extend_configfile" % PATH)
|
||||||
|
def test_extend_configuration(self, mock_extend_configfile):
|
||||||
|
tempest = manager.TempestManager(mock.MagicMock(uuid="uuuiiiddd"))
|
||||||
|
extra_options = mock.Mock()
|
||||||
|
self.assertEqual(mock_extend_configfile.return_value,
|
||||||
|
tempest.extend_configuration(extra_options))
|
||||||
|
mock_extend_configfile.assert_called_once_with(tempest.configfile,
|
||||||
|
extra_options)
|
||||||
|
|
||||||
|
@mock.patch("six.moves.builtins.open", side_effect=mock.mock_open())
|
||||||
|
def test_override_configuration(self, mock_open):
|
||||||
|
tempest = manager.TempestManager(mock.MagicMock(uuid="uuuiiiddd"))
|
||||||
|
new_content = mock.Mock()
|
||||||
|
|
||||||
|
tempest.override_configuration(new_content)
|
||||||
|
|
||||||
|
mock_open.assert_called_once_with(tempest.configfile, "w")
|
||||||
|
mock_open.side_effect().write.assert_called_once_with(new_content)
|
||||||
|
|
||||||
|
@mock.patch("%s.os.path.exists" % PATH)
|
||||||
|
@mock.patch("%s.utils.check_output" % PATH)
|
||||||
|
def test_install_extension(self, mock_check_output, mock_exists):
|
||||||
|
tempest = manager.TempestManager(mock.MagicMock(uuid="uuuiiiddd",
|
||||||
|
system_wide=True))
|
||||||
|
e = self.assertRaises(NotImplementedError, tempest.install_extension,
|
||||||
|
None, None, {"key": "value"})
|
||||||
|
self.assertIn("verifiers don't support extra options", "%s" % e)
|
||||||
|
|
||||||
|
# case #1 system-wide installation
|
||||||
|
source = "https://github.com/example/example"
|
||||||
|
tempest.install_extension(source)
|
||||||
|
|
||||||
|
path = os.path.join(tempest.base_dir, "extensions")
|
||||||
|
mock_check_output.assert_called_once_with(
|
||||||
|
["pip", "install", "--no-deps", "--src", path, "-e",
|
||||||
|
"git+https://github.com/example/example@master#egg=example"],
|
||||||
|
cwd=tempest.base_dir, env=tempest.environ)
|
||||||
|
self.assertFalse(mock_exists.called)
|
||||||
|
|
||||||
|
mock_check_output.reset_mock()
|
||||||
|
|
||||||
|
# case #2 virtual env with specified version
|
||||||
|
tempest.verifier.system_wide = False
|
||||||
|
version = "some"
|
||||||
|
tempest.install_extension(source, version=version)
|
||||||
|
|
||||||
|
test_reqs_path = os.path.join(tempest.base_dir, "extensions",
|
||||||
|
"example", "test-requirements.txt")
|
||||||
|
self.assertEqual([
|
||||||
|
mock.call([
|
||||||
|
"pip", "install", "--src", path, "-e",
|
||||||
|
"git+https://github.com/example/example@some#egg=example"],
|
||||||
|
cwd=tempest.base_dir, env=tempest.environ),
|
||||||
|
mock.call(["pip", "install", "-r", test_reqs_path],
|
||||||
|
cwd=tempest.base_dir, env=tempest.environ)],
|
||||||
|
mock_check_output.call_args_list)
|
||||||
|
mock_exists.assert_called_once_with(test_reqs_path)
|
||||||
|
|
||||||
|
@mock.patch("%s.utils.check_output" % PATH)
|
||||||
|
def test_list_extensions(self, mock_check_output):
|
||||||
|
plugins_list = [
|
||||||
|
{"name": "some", "entry_point": "foo.bar", "location": "/tmp"},
|
||||||
|
{"name": "another", "entry_point": "bar.foo", "location": "/tmp"}
|
||||||
|
]
|
||||||
|
mock_check_output.return_value = json.dumps(plugins_list)
|
||||||
|
|
||||||
|
tempest = manager.TempestManager(mock.MagicMock(uuid="uuuiiiddd"))
|
||||||
|
|
||||||
|
self.assertEqual(plugins_list, tempest.list_extensions())
|
||||||
|
self.assertEqual(1, mock_check_output.call_count)
|
||||||
|
mock_check_output.reset_mock()
|
||||||
|
|
||||||
|
mock_check_output.side_effect = subprocess.CalledProcessError("", "")
|
||||||
|
self.assertRaises(exceptions.RallyException, tempest.list_extensions)
|
||||||
|
self.assertEqual(1, mock_check_output.call_count)
|
||||||
|
|
||||||
|
@mock.patch("%s.TempestManager.list_extensions" % PATH)
|
||||||
|
@mock.patch("%s.os.path.exists" % PATH)
|
||||||
|
@mock.patch("%s.shutil.rmtree" % PATH)
|
||||||
|
def test_uninstall_extension(self, mock_rmtree, mock_exists,
|
||||||
|
mock_list_extensions):
|
||||||
|
plugins_list = [
|
||||||
|
{"name": "some", "entry_point": "foo.bar", "location": "/tmp"},
|
||||||
|
{"name": "another", "entry_point": "bar.foo", "location": "/tmp"}
|
||||||
|
]
|
||||||
|
mock_list_extensions.return_value = plugins_list
|
||||||
|
|
||||||
|
tempest = manager.TempestManager(mock.MagicMock(uuid="uuuiiiddd"))
|
||||||
|
|
||||||
|
tempest.uninstall_extension("some")
|
||||||
|
mock_rmtree.assert_called_once_with(plugins_list[0]["location"])
|
||||||
|
mock_list_extensions.assert_called_once_with()
|
||||||
|
|
||||||
|
mock_rmtree.reset_mock()
|
||||||
|
mock_list_extensions.reset_mock()
|
||||||
|
|
||||||
|
self.assertRaises(exceptions.RallyException,
|
||||||
|
tempest.uninstall_extension, "unexist")
|
||||||
|
|
||||||
|
mock_list_extensions.assert_called_once_with()
|
||||||
|
self.assertFalse(mock_rmtree.called)
|
||||||
|
|
||||||
|
@mock.patch("%s.TempestManager._transform_pattern" % PATH)
|
||||||
|
@mock.patch("%s.testr.TestrLauncher.list_tests" % PATH)
|
||||||
|
def test_list_tests(self, mock_testr_launcher_list_tests,
|
||||||
|
mock__transform_pattern):
|
||||||
|
tempest = manager.TempestManager(mock.MagicMock(uuid="uuuiiiddd"))
|
||||||
|
|
||||||
|
self.assertEqual(mock_testr_launcher_list_tests.return_value,
|
||||||
|
tempest.list_tests())
|
||||||
|
mock_testr_launcher_list_tests.assert_called_once_with("")
|
||||||
|
self.assertFalse(mock__transform_pattern.called)
|
||||||
|
mock_testr_launcher_list_tests.reset_mock()
|
||||||
|
|
||||||
|
pattern = mock.Mock()
|
||||||
|
|
||||||
|
self.assertEqual(mock_testr_launcher_list_tests.return_value,
|
||||||
|
tempest.list_tests(pattern))
|
||||||
|
mock_testr_launcher_list_tests.assert_called_once_with(
|
||||||
|
mock__transform_pattern.return_value)
|
||||||
|
mock__transform_pattern.assert_called_once_with(pattern)
|
||||||
|
|
||||||
|
@mock.patch("%s.testr.TestrLauncher.validate_args" % PATH)
|
||||||
|
def test_validate_args(self, mock_testr_launcher_validate_args):
|
||||||
|
manager.TempestManager.validate_args({})
|
||||||
|
manager.TempestManager.validate_args({"pattern": "some.test"})
|
||||||
|
manager.TempestManager.validate_args({"pattern": "set=smoke"})
|
||||||
|
manager.TempestManager.validate_args({"pattern": "set=compute"})
|
||||||
|
manager.TempestManager.validate_args({"pattern": "set=full"})
|
||||||
|
|
||||||
|
e = self.assertRaises(exceptions.ValidationError,
|
||||||
|
manager.TempestManager.validate_args,
|
||||||
|
{"pattern": "foo=bar"})
|
||||||
|
self.assertEqual("Validation error: 'pattern' argument should be a "
|
||||||
|
"regexp or set name (format: 'tempest.api.identity."
|
||||||
|
"v3', 'set=smoke').", "%s" % e)
|
||||||
|
|
||||||
|
e = self.assertRaises(exceptions.ValidationError,
|
||||||
|
manager.TempestManager.validate_args,
|
||||||
|
{"pattern": "set=foo"})
|
||||||
|
self.assertIn("Test set 'foo' not found in available Tempest test "
|
||||||
|
"sets. Available sets are ", "%s" % e)
|
||||||
|
|
||||||
|
def test__transform_pattern(self):
|
||||||
|
tempest = manager.TempestManager(mock.MagicMock(uuid="uuuiiiddd"))
|
||||||
|
|
||||||
|
self.assertEqual("foo", tempest._transform_pattern("foo"))
|
||||||
|
self.assertEqual("foo=bar", tempest._transform_pattern("foo=bar"))
|
||||||
|
self.assertEqual("", tempest._transform_pattern("set=full"))
|
||||||
|
self.assertEqual("smoke", tempest._transform_pattern("set=smoke"))
|
||||||
|
self.assertEqual("tempest.bar", tempest._transform_pattern("set=bar"))
|
||||||
|
self.assertEqual("tempest.api.compute",
|
||||||
|
tempest._transform_pattern("set=compute"))
|
||||||
|
|
||||||
|
@mock.patch("%s.TempestManager._transform_pattern" % PATH)
|
||||||
|
def test__process_run_args(self, mock__transform_pattern):
|
||||||
|
tempest = manager.TempestManager(mock.MagicMock(uuid="uuuiiiddd"))
|
||||||
|
|
||||||
|
self.assertEqual({}, tempest._process_run_args({}))
|
||||||
|
self.assertFalse(mock__transform_pattern.called)
|
||||||
|
|
||||||
|
self.assertEqual({"foo": "bar"},
|
||||||
|
tempest._process_run_args({"foo": "bar"}))
|
||||||
|
self.assertFalse(mock__transform_pattern.called)
|
||||||
|
|
||||||
|
pattern = mock.Mock()
|
||||||
|
self.assertEqual({"pattern": mock__transform_pattern.return_value},
|
||||||
|
tempest._process_run_args({"pattern": pattern}))
|
||||||
|
mock__transform_pattern.assert_called_once_with(pattern)
|
Loading…
x
Reference in New Issue
Block a user