[Verify] Refactoring of Tempest verifier plugin

The main goal of this patch is to move Tempest context related code
from the rally/plugins/openstack/verification/tempest/config.py file
to the separate rally/plugins/openstack/verification/tempest/context.py
file.

Also,

1. Functions such as 'add_extra_options' and 'extend_configfile' were
   moved from the rally/plugins/openstack/verification/tempest/config.py
   file to rally/verification/utils.py because these functions can be
   used by other verifier plugins in the future.

2. Now Tempest log, test image and lock files are stored in the
   corresponding deployment directory.

3. Now the 'floating_network_name' option from the 'network' section is
   configured as well.

4. A small cleanup was done.

Change-Id: I12c7918400c736b71c5a8044bb1aa53122fb96f1
This commit is contained in:
Yaroslav Lobankov 2017-01-26 18:18:39 +04:00
parent 45c8f44777
commit 435401e008
16 changed files with 970 additions and 983 deletions

View File

@ -98,7 +98,7 @@ Command for Rally 0.7.0 - `rally verify install
$ rally verify install --deployment <uuid> --source <url> --version <vers> \
--system-wide
Command for Rally 0.8.0:
Command since Rally 0.8.0:
.. code-block:: console
@ -133,7 +133,7 @@ Command for Rally 0.7.0 - `rally verify reinstall
$ rally verify reinstall --deployment <uuid> --source <url> --version <vers> \
--system-wide
Command for Rally 0.8.0:
Command since Rally 0.8.0:
.. code-block:: console
@ -159,7 +159,7 @@ Command for Rally 0.7.0 - `rally verify uninstall
$ rally verify uninstall --deployment <uuid>
Command for Rally 0.8.0:
Command since Rally 0.8.0:
.. code-block:: console
@ -185,7 +185,7 @@ Command for Rally 0.7.0 - `rally verify installplugin
$ rally verify installplugin --deployment <uuid> --source <url> \
--version <vers> --system-wide
Command for Rally 0.8.0:
Command since Rally 0.8.0:
.. code-block:: console
@ -214,7 +214,7 @@ Command for Rally 0.7.0 - `rally verify uninstallplugin
$ rally verify uninstallplugin --deployment <uuid> --repo-name <repo_name> \
--system-wide
Command for Rally 0.8.0:
Command since Rally 0.8.0:
.. code-block:: console
@ -239,7 +239,7 @@ Command for Rally 0.7.0 - `rally verify listplugins
$ rally verify listplugins --deployment <uuid> --system-wide
Command for Rally 0.8.0:
Command since Rally 0.8.0:
.. code-block:: console
@ -261,7 +261,7 @@ Command for Rally 0.7.0 - `rally verify discover
$ rally verify discover --deployment <uuid> --system-wide --pattern <pattern>
Command for Rally 0.8.0:
Command since Rally 0.8.0:
.. code-block:: console
@ -294,7 +294,7 @@ Commands for Rally 0.7.0:
$ rally verify showconfig --deployment <uuid>
Command for Rally 0.8.0:
Command since Rally 0.8.0:
.. code-block:: console
@ -332,7 +332,7 @@ Command for Rally 0.7.0 - `rally verify showconfig
$ rally verify showconfig --deployment <uuid>
Command for Rally 0.8.0:
Command since Rally 0.8.0:
.. code-block:: console
@ -357,7 +357,7 @@ Command for Rally 0.7.0 - `rally verify start
--tempest-config <path> --xfail-list <path> --system-wide \
--concurrency <N> --failing --no-use
Command for Rally 0.8.0:
Command since Rally 0.8.0:
.. code-block:: console
@ -407,7 +407,7 @@ Commands for Rally 0.7.0:
$ rally verify detailed --uuid <uuid> --sort-by <query>
Command for Rally 0.8.0:
Command since Rally 0.8.0:
.. code-block:: console
@ -429,7 +429,7 @@ Command for Rally 0.7.0 - `rally verify list
$ rally verify list
Command for Rally 0.8.0:
Command since Rally 0.8.0:
.. code-block:: console
@ -450,7 +450,7 @@ Command for Rally 0.7.0 - `rally verify import
$ rally verify import --deployment <uuid> --set <set_name> --file <path> --no-use
Command for Rally 0.8.0:
Command since Rally 0.8.0:
.. code-block:: console
@ -484,7 +484,7 @@ Commands for Rally 0.7.0:
$ rally verify compare --uuid-1 <uuid_1> --uuid-2 <uuid_2> --csv --html \
--json --output-file <output_file> --threshold <threshold>
Command for Rally 0.8.0:
Command since Rally 0.8.0:
.. code-block:: console

View File

@ -182,7 +182,8 @@ class VerifyCommands(object):
fields.append("Venv location")
formatters["Venv location"] = lambda v: v.manager.venv_dir
cliutils.print_dict(verifier, fields=fields, formatters=formatters,
normalize_field_names=True)
normalize_field_names=True, print_header=False,
table_label="Verifier")
print(_("Attention! All you do in the verifier repository or "
"verifier virtual environment, you do it at your own risk!"))
@ -326,7 +327,7 @@ class VerifyCommands(object):
@cliutils.args("--pattern", dest="pattern", type=str, required=False,
help="Pattern which will be used for matching. Can be a "
"regexp or a verifier-specific entity (for example, "
"in case of Tempest you can specify 'set=smoke'.")
"in case of Tempest you can specify 'set=smoke').")
@envutils.with_default_verifier_id()
@plugins.ensure_plugins_are_loaded
def list_verifier_tests(self, api, verifier_id=None, pattern=""):
@ -399,7 +400,7 @@ class VerifyCommands(object):
@cliutils.args("--pattern", dest="pattern", type=str, required=False,
help="Pattern which will be used for running tests. Can be "
"a regexp or a verifier-specific entity (for example, "
"in case of Tempest you can specify 'set=smoke'.")
"in case of Tempest you can specify 'set=smoke').")
@cliutils.args("--concurrency", dest="concur", type=int, metavar="<N>",
required=False,
help="How many processes to use to run verifier tests. "
@ -658,9 +659,9 @@ class VerifyCommands(object):
"command." % ", ".join(DEFAULT_REPORT_TYPES))
@cliutils.args("--to", dest="output_dest", type=str,
metavar="<dest>", required=False,
help="Report destination. Can be a path to a file (in case"
" of HTML, JSON types) to save the report to or "
"a connection string. It depends on report type.")
help="Report destination. Can be a path to a file (in case "
"of HTML, JSON, etc. types) to save the report to or "
"a connection string. It depends on the report type.")
@cliutils.args("--open", dest="open_it", action="store_true",
required=False, help="Open the output file in a browser.")
@envutils.with_default_verification_uuid
@ -668,7 +669,6 @@ class VerifyCommands(object):
def report(self, api, verification_uuid=None, output_type=None,
output_dest=None, open_it=None):
"""Generate a report for a verification or a few verifications."""
if not isinstance(verification_uuid, list):
verification_uuid = [verification_uuid]

View File

@ -241,10 +241,6 @@ class MultipleMatchesFound(RallyException):
super(MultipleMatchesFound, self).__init__(**kwargs)
class TempestConfigCreationFailure(RallyException):
msg_fmt = _("Unable to create Tempest config file: %(message)s")
class SSHTimeout(RallyException):
pass

View File

@ -29,11 +29,10 @@ from rally.verification import utils
LOG = logging.getLogger(__name__)
CTX_NAME = "verification_testr"
TEST_NAME_RE = re.compile(r"^[a-zA-Z_.0-9]+(\[[a-zA-Z-,=0-9]*\])?$")
@context.configure("testr_verifier", order=999)
@context.configure("testr", order=999)
class TestrContext(context.VerifierContext):
"""Context to transform 'run_args' into CLI arguments for testr."""

View File

@ -1,7 +1,7 @@
[DEFAULT]
debug = True
log_file = tempest.log
use_stderr = False
log_file =
[auth]
use_dynamic_credentials = True
@ -38,11 +38,13 @@ ipv6 = True
[object-storage]
[oslo_concurrency]
lock_path =
[orchestration]
instance_type =
[scenario]
img_dir =
img_file =
[service_available]

View File

@ -15,49 +15,29 @@
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__)
from rally.verification import utils
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. "
@ -68,21 +48,17 @@ TEMPEST_OPTS = [
"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",
@ -107,73 +83,15 @@ CONF.import_opt("glance_image_delete_poll_interval",
"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 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 TempestConfigfileManager(object):
"""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():
@ -235,7 +153,9 @@ class TempestConfigfileManager(utils.RandomNameGeneratorMixin):
net["router:external"] is True]
if public_nets:
net_id = public_nets[0]["id"]
net_name = public_nets[0]["name"]
self.conf.set(section_name, "public_network_id", net_id)
self.conf.set(section_name, "floating_network_name", net_name)
else:
novaclient = self.clients.nova()
net_name = next(net.human_id for net in novaclient.networks.list()
@ -253,22 +173,12 @@ class TempestConfigfileManager(utils.RandomNameGeneratorMixin):
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"]
@ -291,303 +201,21 @@ class TempestConfigfileManager(utils.RandomNameGeneratorMixin):
CONF.tempest.heat_stack_user_role)
def create(self, conf_path, extra_options=None):
self.conf.read(os.path.join(os.path.dirname(__file__), "config.ini"))
for name, method in inspect.getmembers(self, inspect.ismethod):
if name.startswith("_configure_"):
method()
if extra_options:
add_extra_options(extra_options, self.conf)
utils.add_extra_options(extra_options, self.conf)
write_configfile(conf_path, self.conf)
with open(conf_path, "w") as configfile:
self.conf.write(configfile)
return read_configfile(conf_path)
raw_conf = six.StringIO()
raw_conf.write("# Some empty values of options will be replaced while "
"creating required resources (images, flavors, etc).\n")
self.conf.write(raw_conf)
@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 _do_download_image(self, 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 self.clients.glance().images.data(image.id):
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 _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 self._do_download_image(image_path, image)
self._do_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)
return raw_conf.getvalue()

View File

@ -0,0 +1,332 @@
# Copyright 2017: 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 re
import requests
from six.moves import configparser
from rally.common.i18n import _
from rally.common import logging
from rally.common import objects
from rally import exceptions
from rally import osclients
from rally.plugins.openstack.verification.tempest import config as conf
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
from rally.verification import utils
LOG = logging.getLogger(__name__)
@context.configure("tempest", order=900)
class TempestContext(context.VerifierContext):
"""Context class to create/delete resources needed for Tempest."""
RESOURCE_NAME_FORMAT = "rally_verify_XXXXXXXX_XXXXXXXX"
def __init__(self, ctx):
super(TempestContext, self).__init__(ctx)
credential = self.verifier.deployment["admin"]
self.clients = osclients.Clients(objects.Credential(**credential))
self.available_services = self.clients.services().values()
self.conf = configparser.ConfigParser()
self.conf_path = self.verifier.manager.configfile
self.data_dir = self.verifier.manager.home_dir
self.image_name = "tempest-image"
self._created_roles = []
self._created_images = []
self._created_flavors = []
self._created_networks = []
def setup(self):
self.conf.read(self.conf_path)
utils.create_dir(self.data_dir)
self._create_tempest_roles()
self._configure_option("DEFAULT", "log_file",
os.path.join(self.data_dir, "tempest.log"))
self._configure_option("oslo_concurrency", "lock_path",
os.path.join(self.data_dir, "lock_files"))
self._configure_option("scenario", "img_dir", self.data_dir)
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.CONF.tempest.flavor_ref_ram)
self._configure_option("compute", "flavor_ref_alt",
helper_method=self._discover_or_create_flavor,
flv_ram=conf.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.CONF.tempest.heat_instance_type_ram)
with open(self.conf_path, "w") as configfile:
self.conf.write(configfile)
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()
with open(self.conf_path, "w") as configfile:
self.conf.write(configfile)
def _create_tempest_roles(self):
keystoneclient = self.clients.verified_keystone()
roles = [conf.CONF.tempest.swift_operator_role,
conf.CONF.tempest.swift_reseller_admin_role,
conf.CONF.tempest.heat_stack_owner_role,
conf.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 _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' to 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_image(self):
LOG.debug("Trying to discover a public image with name matching "
"regular expression '%s'. Note that case insensitive "
"matching is performed." % conf.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.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.CONF.tempest.img_name_regex)
def _download_image_from_source(self, target_path, image=None):
if image:
LOG.debug("Downloading image '%s' "
"from Glance to %s." % (image.name, target_path))
with open(target_path, "wb") as image_file:
for chunk in self.clients.glance().images.data(image.id):
image_file.write(chunk)
else:
LOG.debug("Downloading image from %s "
"to %s." % (conf.CONF.tempest.img_url, target_path))
try:
response = requests.get(conf.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.RallyException(msg)
if response.status_code == 200:
with open(target_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.RallyException(msg)
LOG.debug("The image has been successfully downloaded!")
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.CONF.tempest.img_name_regex:
image = self._discover_image()
if image:
return self._download_image_from_source(image_path, image)
self._download_image_from_source(image_path)
def _discover_or_create_image(self):
if conf.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.CONF.tempest.img_disk_format,
"container_format": conf.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.CONF.benchmark.glance_image_delete_timeout,
check_interval=conf.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' of option '%s' "
"from Tempest config file." % (opt_value, option))
self.conf.set(section, option, "")
LOG.debug("Value '%s' has been removed." % opt_value)

View File

@ -30,6 +30,8 @@ from rally.verification import utils
LOG = logging.getLogger(__name__)
AVAILABLE_SETS = (list(consts.TempestTestSets) +
list(consts.TempestApiTestSets) +
list(consts.TempestScenarioTestSets))
@ -37,7 +39,7 @@ AVAILABLE_SETS = (list(consts.TempestTestSets) +
@manager.configure(name="tempest", namespace="openstack",
default_repo="https://git.openstack.org/openstack/tempest",
context={"tempest_configuration": {}, "testr_verifier": {}})
context={"tempest": {}, "testr": {}})
class TempestManager(testr.TestrLauncher):
"""Tempest verifier.
@ -67,7 +69,7 @@ class TempestManager(testr.TestrLauncher):
first release after Verification Component redesign)*
"""
RUN_ARGS = {"set": "Name of predefined sets of tests. Known names: %s"
RUN_ARGS = {"set": "Name of predefined set of tests. Known names: %s"
% ", ".join(AVAILABLE_SETS)}
@property
@ -84,27 +86,43 @@ class TempestManager(testr.TestrLauncher):
def configfile(self):
return os.path.join(self.home_dir, "tempest.conf")
def get_configuration(self):
"""Get Tempest configuration."""
return config.read_configfile(self.configfile)
def validate_args(self, args):
"""Validate given arguments."""
super(TempestManager, self).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":
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 configure(self, extra_options=None):
"""Configure Tempest."""
if not os.path.isdir(self.home_dir):
os.makedirs(self.home_dir)
cm = config.TempestConfigfileManager(self.verifier.deployment)
raw_configfile = cm.create(self.configfile, extra_options)
return raw_configfile
utils.create_dir(self.home_dir)
tcm = config.TempestConfigfileManager(self.verifier.deployment)
return tcm.create(self.configfile, extra_options)
def is_configured(self):
"""Check whether Tempest is configured or not."""
return os.path.exists(self.configfile)
def get_configuration(self):
"""Get Tempest configuration."""
with open(self.configfile) as f:
return f.read()
def extend_configuration(self, extra_options):
"""Extend Tempest configuration with extra options."""
return config.extend_configfile(self.configfile, extra_options)
return utils.extend_configfile(extra_options, self.configfile)
def override_configuration(self, new_configuration):
"""Override Tempest configuration by new configuration."""
@ -181,26 +199,14 @@ class TempestManager(testr.TestrLauncher):
pattern = self._transform_pattern(pattern)
return super(TempestManager, self).list_tests(pattern)
def validate_args(self, args):
"""Validate given arguments."""
super(TempestManager, self).validate_args(args)
def prepare_run_args(self, run_args):
"""Prepare 'run_args' for testr context."""
if run_args.get("pattern"):
run_args["pattern"] = self._transform_pattern(run_args["pattern"])
return run_args
if args.get("pattern"):
pattern = args["pattern"].split("=", 1)
if len(pattern) == 1:
pass # it is just a regex
elif pattern[0] == "set":
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):
@staticmethod
def _transform_pattern(pattern):
"""Transform pattern into Tempest-specific pattern."""
parsed_pattern = pattern.split("=", 1)
if len(parsed_pattern) == 2:
@ -213,9 +219,3 @@ class TempestManager(testr.TestrLauncher):
return "tempest.%s" % parsed_pattern[1]
return pattern # it is just a regex
def prepare_run_args(self, run_args):
"""Prepare 'run_args' for testr context."""
if run_args.get("pattern"):
run_args["pattern"] = self._transform_pattern(run_args["pattern"])
return run_args

View File

@ -104,7 +104,7 @@
<code>$ {{ compare.json.cmd }}</code>
<span class="{{ compare.junit_xml.status }}">[{{ compare.junit_xml.status }}]</span>
<a href="{{ compare.junit_xml.output_file }}">Generate the trends report for two verifications in JUnit-XML format</a> [<a href="{{ compare.junit_xml.stdout_file }}">Output from CLI</a>]
<code>$ {{ compare.junit_xml.cmd }}</code><br>
<code>$ {{ compare.junit_xml.cmd }}</code>
<span class="{{ compare.html.status }}">[{{ compare.html.status }}]</span>
<a href="{{ compare.html.output_file }}">Generate the trends report for two verifications in HTML format</a> [<a href="{{ compare.html.stdout_file }}">Output from CLI</a>]
<code>$ {{ compare.html.cmd }}</code><br>

View File

@ -292,6 +292,10 @@ class VerifierManager(plugin.Plugin):
"""Check whether a verifier is configured or not."""
return True
def get_configuration(self):
"""Get verifier configuration (e.g., the config file content)."""
return ""
def override_configuration(self, new_configuration):
"""Override verifier configuration.
@ -317,10 +321,6 @@ class VerifierManager(plugin.Plugin):
_LE("'%s' verifiers don't support configuration at all.")
% self.get_name())
def get_configuration(self):
"""Get verifier configuration (e.g., the config file content)."""
return ""
def install_extension(self, source, version=None, extra_settings=None):
"""Install a verifier extension.

View File

@ -12,11 +12,12 @@
# License for the specific language governing permissions and limitations
# under the License.
import errno
import os
import subprocess
from oslo_utils import encodeutils
import six
from six.moves import configparser
from rally.common import logging
@ -64,10 +65,31 @@ def check_output(*args, **kwargs):
def create_dir(dir_path):
try:
if not os.path.isdir(dir_path):
os.makedirs(dir_path)
except OSError as exc:
if exc.errno == errno.EEXIST and os.path.isdir(dir_path):
# directory already exists
return
raise
return dir_path
def extend_configfile(extra_options, conf_path):
conf_object = configparser.ConfigParser()
conf_object.read(conf_path)
conf_object = add_extra_options(extra_options, conf_object)
with open(conf_path, "w") as configfile:
conf_object.write(configfile)
raw_conf = six.StringIO()
conf_object.write(raw_conf)
return raw_conf.getvalue()
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

View File

@ -115,8 +115,8 @@ class VerifyCommandsTestCase(test.TestCase):
self.assertEqual(1, len(print_dict_calls))
self.assertEqual(
"+----------------+----------------------------+\n"
"| Property | Value |\n"
"+---------------------------------------------+\n"
"| Verifier |\n"
"+----------------+----------------------------+\n"
"| UUID | v_uuid |\n"
"| Status | installed |\n"

View File

@ -13,18 +13,14 @@
# 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
@ -48,53 +44,43 @@ PATH = "rally.plugins.openstack.verification.tempest.config"
@ddt.ddt
class TempestConfigTestCase(test.TestCase):
class TempestConfigfileManagerTestCase(test.TestCase):
def setUp(self):
super(TempestConfigTestCase, self).setUp()
super(TempestConfigfileManagerTestCase, 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")
self.tempest = config.TempestConfigfileManager(CREDS)
def test__configure_auth(self):
self.tempest_conf._configure_auth()
self.tempest.conf.add_section("auth")
self.tempest._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")
result = self.tempest.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.available_services = ["sahara"]
self.tempest_conf.clients.services.return_value = {
self.tempest.clients.services.return_value = {
service_type: "sahara"}
self.tempest_conf._configure_data_processing()
self.tempest.conf.add_section("data-processing")
self.tempest._configure_data_processing()
self.assertEqual(
self.tempest_conf.conf.get(
self.tempest.conf.get(
"data-processing", "catalog_type"), service_type)
def test__configure_identity(self):
self.tempest_conf._configure_identity()
self.tempest.conf.add_section("identity")
self.tempest._configure_identity()
expected = (
("region", CREDS["admin"]["region_name"]),
@ -104,45 +90,51 @@ class TempestConfigTestCase(test.TestCase):
("disable_ssl_certificate_validation",
str(CREDS["admin"]["https_insecure"])),
("ca_certificates_file", CREDS["admin"]["https_cacert"]))
result = self.tempest_conf.conf.items("identity")
result = self.tempest.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()
self.tempest.available_services = ["neutron"]
client = self.tempest.clients.neutron()
client.list_networks.return_value = {
"networks": [
{
"status": "ACTIVE",
"id": "test_id",
"name": "test_name",
"router:external": True
}
]
}
self.tempest_conf._configure_network()
self.assertEqual(
self.tempest_conf.conf.get("network",
"public_network_id"), "test_id")
self.tempest.conf.add_section("network")
self.tempest._configure_network()
self.assertEqual(self.tempest.conf.get("network", "public_network_id"),
"test_id")
self.assertEqual(self.tempest.conf.get("network",
"floating_network_name"),
"test_name")
def test__configure_network_if_nova(self):
self.tempest_conf.available_services = ["nova"]
client = self.tempest_conf.clients.nova()
self.tempest.available_services = ["nova"]
client = self.tempest.clients.nova()
client.networks.list.return_value = [
mock.MagicMock(human_id="fake-network")]
self.tempest_conf._configure_network()
self.tempest.conf.add_section("compute")
self.tempest.conf.add_section("validation")
self.tempest._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)
result = self.tempest.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()
self.tempest.available_services = ["neutron"]
client = self.tempest.clients.neutron()
client.list_ext.return_value = {
"extensions": [
{"alias": "dvr"},
@ -151,63 +143,47 @@ class TempestConfigTestCase(test.TestCase):
]
}
self.tempest_conf._configure_network_feature_enabled()
self.tempest.conf.add_section("network-feature-enabled")
self.tempest._configure_network_feature_enabled()
client.list_ext.assert_called_once_with("extensions", "/extensions",
retrieve_all=True)
self.assertEqual(self.tempest_conf.conf.get(
self.assertEqual(self.tempest.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()
self.tempest.conf.add_section("object-storage")
self.tempest._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")
result = self.tempest.conf.items("object-storage")
for item in expected:
self.assertIn(item, result)
def test__configure_orchestration(self):
self.tempest_conf._configure_orchestration()
self.tempest.conf.add_section("orchestration")
self.tempest._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")
result = self.tempest.conf.items("orchestration")
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()
self.tempest.available_services = available_services
self.tempest.conf.add_section("service_available")
self.tempest._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")
result = self.tempest.conf.items("service_available")
for item in expected:
self.assertIn(item, result)
@ -215,427 +191,34 @@ class TempestConfigTestCase(test.TestCase):
@ddt.unpack
def test__configure_validation(self, service="nova",
connect_method="fixed"):
self.tempest_conf.available_services = [service]
self.tempest_conf._configure_validation()
self.tempest.available_services = [service]
self.tempest.conf.add_section("validation")
self.tempest._configure_validation()
expected = (("run_validation", "True"),
("connect_method", connect_method))
result = self.tempest_conf.conf.items("validation")
expected = (("connect_method", connect_method), )
result = self.tempest.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("%s.six.StringIO" % PATH)
@mock.patch("six.moves.builtins.open", side_effect=mock.mock_open())
@mock.patch("inspect.getmembers")
def test_create(self, mock_inspect_getmembers, mock_write_configfile,
mock_read_configfile):
def test_create(self, mock_inspect_getmembers, mock_open, mock_string_io):
configure_something_method = mock.MagicMock()
mock_inspect_getmembers.return_value = [("_configure_something",
configure_something_method)]
self.tempest.conf.read = mock.Mock()
self.tempest.conf.write = mock.Mock()
self.tempest.conf.read.return_value = "[section]\noption = value"
fake_extra_conf = {"section": {"option": "value"}}
fake_extra_conf = {"section2": {"option2": "value2"}}
self.tempest.create("/path/to/fake/conf", fake_extra_conf)
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()
glanceclient = self.context.clients.glance()
glanceclient.images.data.return_value = "data"
self.context._do_download_image(img_path, img)
mock_open.assert_called_once_with(img_path, "wb")
glanceclient.images.data.assert_called_once_with(img.id)
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"
self.context._do_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,
self.context._do_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,
self.context._do_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"
glanceclient = self.context.clients.glance()
glanceclient.images.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_wrap.return_value.list_images.assert_called_once_with(
status="active", visibility="public")
glanceclient.images.data.assert_called_once_with(img_2.id)
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)
self.assertIn(("option2", "value2"),
self.tempest.conf.items("section2"))
mock_open.assert_called_once_with("/path/to/fake/conf", "w")
self.tempest.conf.write.assert_has_calls(
[mock.call(mock_open.side_effect()),
mock.call(mock_string_io.return_value)])
mock_string_io.return_value.getvalue.assert_called_once_with()

View File

@ -0,0 +1,409 @@
# Copyright 2017: 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 rally.plugins.openstack.verification.tempest import context
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.context"
@ddt.ddt
class TempestContextTestCase(test.TestCase):
def setUp(self):
super(TempestContextTestCase, 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.home_dir = "/p/a/t/h"
cfg["verifier"].manager.configfile = "/fake/path/to/config"
self.context = context.TempestContext(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()
glanceclient = self.context.clients.glance()
glanceclient.images.data.return_value = "data"
self.context._download_image_from_source(img_path, img)
mock_open.assert_called_once_with(img_path, "wb")
glanceclient.images.data.assert_called_once_with(img.id)
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"
self.context._download_image_from_source(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.RallyException,
self.context._download_image_from_source,
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.RallyException,
self.context._download_image_from_source,
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"
glanceclient = self.context.clients.glance()
glanceclient.images.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_wrap.return_value.list_images.assert_called_once_with(
status="active", visibility="public")
glanceclient.images.data.assert_called_once_with(img_2.id)
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("six.moves.builtins.open", side_effect=mock.mock_open())
@mock.patch("%s.TempestContext._configure_option" % PATH)
@mock.patch("%s.TempestContext._create_tempest_roles" % PATH)
@mock.patch("rally.verification.utils.create_dir")
@mock.patch("%s.osclients.Clients" % PATH)
def test_setup(self, mock_clients, mock_create_dir,
mock__create_tempest_roles, mock__configure_option,
mock_open):
verifier = mock.MagicMock(deployment=CREDS)
verifier.manager.home_dir = "/p/a/t/h"
# case #1: no neutron and heat
mock_clients.return_value.services.return_value = {}
ctx = context.TempestContext({"verifier": verifier})
ctx.conf = mock.Mock()
ctx.setup()
ctx.conf.read.assert_called_once_with(verifier.manager.configfile)
mock_create_dir.assert_called_once_with(ctx.data_dir)
mock__create_tempest_roles.assert_called_once_with()
mock_open.assert_called_once_with(verifier.manager.configfile, "w")
ctx.conf.write(mock_open.side_effect())
self.assertEqual(
[mock.call("DEFAULT", "log_file", "/p/a/t/h/tempest.log"),
mock.call("oslo_concurrency", "lock_path", "/p/a/t/h/lock_files"),
mock.call("scenario", "img_dir", "/p/a/t/h"),
mock.call("scenario", "img_file", ctx.image_name,
helper_method=ctx._download_image),
mock.call("compute", "image_ref",
helper_method=ctx._discover_or_create_image),
mock.call("compute", "image_ref_alt",
helper_method=ctx._discover_or_create_image),
mock.call("compute", "flavor_ref",
helper_method=ctx._discover_or_create_flavor,
flv_ram=config.CONF.tempest.flavor_ref_ram),
mock.call("compute", "flavor_ref_alt",
helper_method=ctx._discover_or_create_flavor,
flv_ram=config.CONF.tempest.flavor_ref_alt_ram)],
mock__configure_option.call_args_list)
mock_create_dir.reset_mock()
mock__create_tempest_roles.reset_mock()
mock_open.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"}
ctx = context.TempestContext({"verifier": verifier})
ctx.conf = mock.Mock()
ctx.setup()
ctx.conf.read.assert_called_once_with(verifier.manager.configfile)
mock_create_dir.assert_called_once_with(ctx.data_dir)
mock__create_tempest_roles.assert_called_once_with()
mock_open.assert_called_once_with(verifier.manager.configfile, "w")
ctx.conf.write(mock_open.side_effect())
self.assertEqual(
[mock.call("DEFAULT", "log_file", "/p/a/t/h/tempest.log"),
mock.call("oslo_concurrency", "lock_path", "/p/a/t/h/lock_files"),
mock.call("scenario", "img_dir", "/p/a/t/h"),
mock.call("scenario", "img_file", ctx.image_name,
helper_method=ctx._download_image),
mock.call("compute", "image_ref",
helper_method=ctx._discover_or_create_image),
mock.call("compute", "image_ref_alt",
helper_method=ctx._discover_or_create_image),
mock.call("compute", "flavor_ref",
helper_method=ctx._discover_or_create_flavor,
flv_ram=config.CONF.tempest.flavor_ref_ram),
mock.call("compute", "flavor_ref_alt",
helper_method=ctx._discover_or_create_flavor,
flv_ram=config.CONF.tempest.flavor_ref_alt_ram),
mock.call("compute", "fixed_network_name",
helper_method=ctx._create_network_resources),
mock.call("orchestration", "instance_type",
helper_method=ctx._discover_or_create_flavor,
flv_ram=config.CONF.tempest.heat_instance_type_ram)],
mock__configure_option.call_args_list)

View File

@ -26,6 +26,7 @@ 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()
@ -43,12 +44,13 @@ class TempestManagerTestCase(test.TestCase):
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):
@mock.patch("six.moves.builtins.open", side_effect=mock.mock_open())
def test_get_configuration(self, mock_open):
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)
tempest.get_configuration()
mock_open.assert_called_once_with(tempest.configfile)
mock_open.side_effect().read.assert_called_once_with()
@mock.patch("%s.config.TempestConfigfileManager" % PATH)
def test_configure(self, mock_tempest_configfile_manager):
@ -67,14 +69,14 @@ class TempestManagerTestCase(test.TestCase):
tempest = manager.TempestManager(mock.MagicMock(uuid="uuuiiiddd"))
self.assertTrue(tempest.is_configured())
@mock.patch("%s.config.extend_configfile" % PATH)
@mock.patch("rally.verification.utils.extend_configfile")
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_extend_configfile.assert_called_once_with(extra_options,
tempest.configfile)
@mock.patch("six.moves.builtins.open", side_effect=mock.mock_open())
def test_override_configuration(self, mock_open):

View File

@ -12,45 +12,27 @@
# License for the specific language governing permissions and limitations
# under the License.
import errno
import subprocess
import mock
from six.moves import configparser
from rally.verification import utils
from tests.unit import test
class SomeException(OSError):
def __init__(self, errno):
super(SomeException, self).__init__()
self.errno = errno
class UtilsTestCase(test.TestCase):
@mock.patch("rally.verification.utils.os")
def test_create_dir(self, mock_os):
@mock.patch("rally.verification.utils.os.makedirs")
@mock.patch("rally.verification.utils.os.path.isdir",
side_effect=[False, True])
def test_create_dir(self, mock_isdir, mock_makedirs):
utils.create_dir("some")
mock_os.makedirs.assert_called_once_with("some")
mock_makedirs.assert_called_once_with("some")
# directory exists
mock_os.makedirs.reset_mock()
mock_os.makedirs.side_effect = SomeException(errno=errno.EEXIST)
mock_os.path.isdir.return_value = True
mock_makedirs.reset_mock()
utils.create_dir("some")
mock_os.makedirs.assert_called_once_with("some")
# directory doesn't exist
mock_os.makedirs.reset_mock()
mock_os.makedirs.side_effect = SomeException(errno=666)
self.assertRaises(SomeException, utils.create_dir, "some")
mock_os.makedirs.assert_called_once_with("some")
mock_os.makedirs.reset_mock()
mock_os.makedirs.side_effect = SomeException(errno=errno.EEXIST)
mock_os.path.isdir.return_value = False
self.assertRaises(SomeException, utils.create_dir, "some")
mock_os.makedirs.assert_called_once_with("some")
mock_makedirs.assert_not_called()
@mock.patch("rally.verification.utils.encodeutils")
@mock.patch("rally.verification.utils.LOG")
@ -73,3 +55,35 @@ class UtilsTestCase(test.TestCase):
msg_on_err=msg)
self.assertEqual(3, mock_log.error.call_count)
mock_log.error.assert_any_call(msg)
@mock.patch("rally.verification.utils.six.StringIO")
@mock.patch("rally.verification.utils.add_extra_options")
@mock.patch("rally.verification.utils.configparser.ConfigParser")
@mock.patch("six.moves.builtins.open", side_effect=mock.mock_open())
def test_extend_configfile(self, mock_open, mock_config_parser,
mock_add_extra_options, mock_string_io):
extra_options = mock.Mock()
conf_path = "/path/to/fake/conf"
utils.extend_configfile(extra_options, conf_path)
conf = mock_config_parser.return_value
conf.read.assert_called_once_with(conf_path)
mock_add_extra_options.assert_called_once_with(extra_options, conf)
conf = mock_add_extra_options.return_value
conf.write.assert_has_calls([mock.call(mock_open.side_effect()),
mock.call(mock_string_io.return_value)])
mock_string_io.return_value.getvalue.assert_called_once_with()
def test_add_extra_options(self):
conf = configparser.ConfigParser()
extra_options = {"section": {"foo": "bar"},
"section2": {"option": "value"}}
conf = utils.add_extra_options(extra_options, conf)
expected = {"section": ("foo", "bar"), "section2": ("option", "value")}
for section, option in expected.items():
result = conf.items(section)
self.assertIn(option, result)