Merge "Add purge flag in image_meta_to_headers"
diff --git a/.gitignore b/.gitignore
index 5b87cec..e96deb1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,6 +23,7 @@
!.coveragerc
cover/
doc/source/_static/tempest.conf.sample
+doc/source/plugin-registry.rst
# Files created by releasenotes build
releasenotes/build
diff --git a/README.rst b/README.rst
index 725a890..fe65dcd 100644
--- a/README.rst
+++ b/README.rst
@@ -70,12 +70,12 @@
it's recommended that you copy or rename tempest.conf.sample to tempest.conf
and make those changes to that file in /etc/tempest
-#. Setup a local working Tempest dir. This is done by using the tempest init
+#. Setup a local Tempest workspace. This is done by using the tempest init
command::
$ tempest init cloud-01
- works the same as::
+ which also works the same as::
$ mkdir cloud-01 && cd cloud-01 && tempest init
@@ -91,8 +91,19 @@
any changes to it otherwise Tempest will not know how to load it.
#. Once the configuration is done you're now ready to run Tempest. This can
- be done with testr directly or any `testr`_ based test runner, like
- `ostestr`_. For example, from the working dir running::
+ be done using the :ref:`tempest_run` command. This can be done by either
+ running::
+
+ $ tempest run
+
+ from the Tempest workspace directory. Or you can use the ``--workspace``
+ argument to run in the workspace you created regarless of your current
+ working directory. For example::
+
+ $ tempest run --workspace cloud-01
+
+ There is also the option to use testr directly, or any `testr`_ based test
+ runner, like `ostestr`_. For example, from the workspace dir run::
$ ostestr --regex '(?!.*\[.*\bslow\b.*\])(^tempest\.(api|scenario))'
diff --git a/doc/source/conf.py b/doc/source/conf.py
index eef3620..127613d 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -15,6 +15,17 @@
import os
import subprocess
+# Build the plugin registry
+def build_plugin_registry(app):
+ root_dir = os.path.dirname(
+ os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+ subprocess.call(['tools/generate-tempest-plugins-list.sh'], cwd=root_dir)
+
+def setup(app):
+ app.connect('builder-inited', build_plugin_registry)
+
+
+
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
diff --git a/doc/source/index.rst b/doc/source/index.rst
index 60ff46b..f1ede06 100644
--- a/doc/source/index.rst
+++ b/doc/source/index.rst
@@ -2,19 +2,14 @@
Tempest Testing Project
=======================
-Contents:
+--------
+Overview
+--------
.. toctree::
:maxdepth: 2
overview
- HACKING
- REVIEWING
- plugin
- plugin-registry
- library
- microversion_testing
- test-removal
------------
Field Guides
@@ -32,6 +27,10 @@
field_guide/stress
field_guide/unit_tests
+=========
+For users
+=========
+
---------------------------
Tempest Configuration Guide
---------------------------
@@ -56,6 +55,41 @@
workspace
run
+==============
+For developers
+==============
+
+-----------
+Development
+-----------
+
+.. toctree::
+ :maxdepth: 2
+
+ HACKING
+ REVIEWING
+ microversion_testing
+ test-removal
+
+-------
+Plugins
+-------
+
+.. toctree::
+ :maxdepth: 2
+
+ plugin
+ plugin-registry
+
+-------
+Library
+-------
+
+.. toctree::
+ :maxdepth: 2
+
+ library
+
==================
Indices and tables
==================
diff --git a/doc/source/plugin-registry.rst b/doc/source/plugin-registry.rst
deleted file mode 100644
index 517e5b8..0000000
--- a/doc/source/plugin-registry.rst
+++ /dev/null
@@ -1,23 +0,0 @@
-..
- Note to patch submitters: this file is covered by a periodic proposal
- job. You should edit the files data/tempest-plugins-registry.footer
- data/tempest-plugins-registry.header instead of this one.
-
-==========================
- Tempest Plugin Registry
-==========================
-
-Since we've created the external plugin mechanism, it's gotten used by
-a lot of projects. The following is a list of plugins that currently
-exist.
-
-Detected Plugins
-================
-
-The following will list plugins that a script has found in the openstack/
-namespace, which includes but is not limited to official OpenStack
-projects.
-
-+----------------------------+-------------------------------------------------------------------------+
-|Plugin Name |URL |
-+----------------------------+-------------------------------------------------------------------------+
diff --git a/doc/source/run.rst b/doc/source/run.rst
index 07fa5f7..ce7f03e 100644
--- a/doc/source/run.rst
+++ b/doc/source/run.rst
@@ -1,3 +1,5 @@
+.. _tempest_run:
+
-----------
Tempest Run
-----------
diff --git a/requirements.txt b/requirements.txt
index 7d01f69..84be219 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -14,7 +14,7 @@
oslo.i18n>=2.1.0 # Apache-2.0
oslo.log>=1.14.0 # Apache-2.0
oslo.serialization>=1.10.0 # Apache-2.0
-oslo.utils>=3.11.0 # Apache-2.0
+oslo.utils>=3.14.0 # Apache-2.0
six>=1.9.0 # MIT
fixtures>=3.0.0 # Apache-2.0/BSD
testscenarios>=0.4 # Apache-2.0/BSD
diff --git a/tempest/api/identity/admin/v3/test_projects.py b/tempest/api/identity/admin/v3/test_projects.py
index 607bebe..b53cfae 100644
--- a/tempest/api/identity/admin/v3/test_projects.py
+++ b/tempest/api/identity/admin/v3/test_projects.py
@@ -13,10 +13,15 @@
# License for the specific language governing permissions and limitations
# under the License.
+import testtools
+
from tempest.api.identity import base
from tempest.common.utils import data_utils
+from tempest import config
from tempest import test
+CONF = config.CONF
+
class ProjectsTestJSON(base.BaseIdentityV3AdminTest):
@@ -52,6 +57,37 @@
self.assertEqual(project_name, body['name'])
self.assertEqual(self.data.domain['id'], body['domain_id'])
+ @testtools.skipUnless(CONF.identity_feature_enabled.reseller,
+ 'Reseller not available.')
+ @test.idempotent_id('1854f9c0-70bc-4d11-a08a-1c789d339e3d')
+ def test_project_create_with_parent(self):
+ # Create root project without providing a parent_id
+ self.data.setup_test_domain()
+ domain_id = self.data.domain['id']
+
+ root_project_name = data_utils.rand_name('root_project')
+ root_project = self.projects_client.create_project(
+ root_project_name, domain_id=domain_id)['project']
+ self.addCleanup(
+ self.projects_client.delete_project, root_project['id'])
+
+ root_project_id = root_project['id']
+ parent_id = root_project['parent_id']
+ self.assertEqual(root_project_name, root_project['name'])
+ # If not provided, the parent_id must point to the top level
+ # project in the hierarchy, i.e. its domain
+ self.assertEqual(domain_id, parent_id)
+
+ # Create a project using root_project_id as parent_id
+ project_name = data_utils.rand_name('project')
+ project = self.projects_client.create_project(
+ project_name, domain_id=domain_id,
+ parent_id=root_project_id)['project']
+ self.data.projects.append(project)
+ parent_id = project['parent_id']
+ self.assertEqual(project_name, project['name'])
+ self.assertEqual(root_project_id, parent_id)
+
@test.idempotent_id('1f66dc76-50cc-4741-a200-af984509e480')
def test_project_create_enabled(self):
# Create a project that is enabled
diff --git a/tempest/api/identity/base.py b/tempest/api/identity/base.py
index 7a1e3a5..df39390 100644
--- a/tempest/api/identity/base.py
+++ b/tempest/api/identity/base.py
@@ -218,21 +218,21 @@
def teardown_all(self):
for user in self.users:
test_utils.call_and_ignore_notfound_exc(
- self.users_client.delete_user, user)
+ self.users_client.delete_user, user['id'])
for tenant in self.tenants:
test_utils.call_and_ignore_notfound_exc(
- self.projects_client.delete_tenant, tenant)
+ self.projects_client.delete_tenant, tenant['id'])
for project in reversed(self.projects):
test_utils.call_and_ignore_notfound_exc(
- self.projects_client.delete_project, project)
+ self.projects_client.delete_project, project['id'])
for role in self.roles:
test_utils.call_and_ignore_notfound_exc(
- self.roles_client.delete_role, role)
+ self.roles_client.delete_role, role['id'])
for domain in self.domains:
test_utils.call_and_ignore_notfound_exc(
- self.domains_client.update_domain, domain, enabled=False)
+ self.domains_client.update_domain, domain['id'], enabled=False)
test_utils.call_and_ignore_notfound_exc(
- self.domains_client.delete_domain, domain)
+ self.domains_client.delete_domain, domain['id'])
class DataGeneratorV2(BaseDataGenerator):
diff --git a/tempest/api/network/test_allowed_address_pair.py b/tempest/api/network/test_allowed_address_pair.py
index b2892e5..92dfc56 100644
--- a/tempest/api/network/test_allowed_address_pair.py
+++ b/tempest/api/network/test_allowed_address_pair.py
@@ -14,6 +14,7 @@
# under the License.
import netaddr
+import six
from tempest.api.network import base
from tempest import config
@@ -90,7 +91,8 @@
body = self.ports_client.update_port(
port_id, allowed_address_pairs=allowed_address_pairs)
allowed_address_pair = body['port']['allowed_address_pairs']
- self.assertEqual(allowed_address_pair, allowed_address_pairs)
+ six.assertCountEqual(self, allowed_address_pair,
+ allowed_address_pairs)
@test.idempotent_id('9599b337-272c-47fd-b3cf-509414414ac4')
def test_update_port_with_address_pair(self):
diff --git a/tempest/api/volume/test_volumes_clone.py b/tempest/api/volume/test_volumes_clone.py
new file mode 100644
index 0000000..f38a068
--- /dev/null
+++ b/tempest/api/volume/test_volumes_clone.py
@@ -0,0 +1,44 @@
+# Copyright 2016 OpenStack Foundation
+# 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 tempest.api.volume import base
+from tempest import config
+from tempest import test
+
+
+CONF = config.CONF
+
+
+class VolumesCloneTest(base.BaseVolumeTest):
+
+ @test.idempotent_id('9adae371-a257-43a5-9555-dc7c88e66e0e')
+ def test_create_from_volume(self):
+ # Creates a volume from another volume passing a size different from
+ # the source volume.
+ src_size = CONF.volume.volume_size
+
+ src_vol = self.create_volume(size=src_size)
+ # Destination volume bigger than source
+ dst_vol = self.create_volume(source_volid=src_vol['id'],
+ size=src_size + 1)
+
+ volume = self.volumes_client.show_volume(dst_vol['id'])['volume']
+ # Should allow
+ self.assertEqual(volume['source_volid'], src_vol['id'])
+ self.assertEqual(int(volume['size']), src_size + 1)
+
+
+class VolumesV1CloneTest(VolumesCloneTest):
+ _api_version = 1
diff --git a/tempest/api/volume/test_volumes_clone_negative.py b/tempest/api/volume/test_volumes_clone_negative.py
new file mode 100644
index 0000000..ee51e00
--- /dev/null
+++ b/tempest/api/volume/test_volumes_clone_negative.py
@@ -0,0 +1,42 @@
+# Copyright 2016 OpenStack Foundation
+# 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 tempest.api.volume import base
+from tempest import config
+from tempest.lib import exceptions
+from tempest import test
+
+
+CONF = config.CONF
+
+
+class VolumesCloneTest(base.BaseVolumeTest):
+
+ @test.idempotent_id('9adae371-a257-43a5-459a-dc7c88e66e0e')
+ def test_create_from_volume_decreasing_size(self):
+ # Creates a volume from another volume passing a size different from
+ # the source volume.
+ src_size = CONF.volume.volume_size + 1
+ src_vol = self.create_volume(size=src_size)
+
+ # Destination volume smaller than source
+ self.assertRaises(exceptions.BadRequest,
+ self.volumes_client.create_volume,
+ size=src_size - 1,
+ source_volid=src_vol['id'])
+
+
+class VolumesV1CloneTest(VolumesCloneTest):
+ _api_version = 1
diff --git a/tempest/api/volume/test_volumes_snapshots.py b/tempest/api/volume/test_volumes_snapshots.py
index 0f7c4f6..c7f1e6e 100644
--- a/tempest/api/volume/test_volumes_snapshots.py
+++ b/tempest/api/volume/test_volumes_snapshots.py
@@ -178,16 +178,19 @@
@test.idempotent_id('677863d1-3142-456d-b6ac-9924f667a7f4')
def test_volume_from_snapshot(self):
- # Create a temporary snap using wrapper method from base, then
- # create a snap based volume and deletes it
- snapshot = self.create_snapshot(self.volume_origin['id'])
- # NOTE(gfidente): size is required also when passing snapshot_id
- volume = self.volumes_client.create_volume(
- snapshot_id=snapshot['id'])['volume']
- waiters.wait_for_volume_status(self.volumes_client,
- volume['id'], 'available')
- self.delete_volume(self.volumes_client, volume['id'])
- self.cleanup_snapshot(snapshot)
+ # Creates a volume a snapshot passing a size different from the source
+ src_size = CONF.volume.volume_size
+
+ src_vol = self.create_volume(size=src_size)
+ src_snap = self.create_snapshot(src_vol['id'])
+ # Destination volume bigger than source snapshot
+ dst_vol = self.create_volume(snapshot_id=src_snap['id'],
+ size=src_size + 1)
+
+ volume = self.volumes_client.show_volume(dst_vol['id'])['volume']
+ # Should allow
+ self.assertEqual(volume['snapshot_id'], src_snap['id'])
+ self.assertEqual(int(volume['size']), src_size + 1)
@test.idempotent_id('db4d8e0a-7a2e-41cc-a712-961f6844e896')
def test_snapshot_list_param_limit(self):
diff --git a/tempest/api/volume/test_volumes_snapshots_negative.py b/tempest/api/volume/test_volumes_snapshots_negative.py
index 374979c..2df9523 100644
--- a/tempest/api/volume/test_volumes_snapshots_negative.py
+++ b/tempest/api/volume/test_volumes_snapshots_negative.py
@@ -46,6 +46,20 @@
self.snapshots_client.create_snapshot,
volume_id=None, display_name=s_name)
+ @test.idempotent_id('677863d1-34f9-456d-b6ac-9924f667a7f4')
+ def test_volume_from_snapshot_decreasing_size(self):
+ # Creates a volume a snapshot passing a size different from the source
+ src_size = CONF.volume.volume_size + 1
+
+ src_vol = self.create_volume(size=src_size)
+ src_snap = self.create_snapshot(src_vol['id'])
+
+ # Destination volume smaller than source
+ self.assertRaises(lib_exc.BadRequest,
+ self.volumes_client.create_volume,
+ size=src_size - 1,
+ snapshot_id=src_snap['id'])
+
class VolumesV1SnapshotNegativeTestJSON(VolumesV2SnapshotNegativeTestJSON):
_api_version = 1
diff --git a/tempest/cmd/run.py b/tempest/cmd/run.py
index 5580cf7..5270cac 100644
--- a/tempest/cmd/run.py
+++ b/tempest/cmd/run.py
@@ -54,6 +54,24 @@
If you want to adjust the number of workers use the **--concurrency** option
and if you want to run tests serially use **--serial**
+Running with Workspaces
+-----------------------
+Tempest run enables you to run your tempest tests from any setup tempest
+workspace it relies on you having setup a tempest workspace with either the
+``tempest init`` or ``tempest workspace`` commands. Then using the
+``--workspace`` CLI option you can specify which one of your workspaces you
+want to run tempest from. Using this option you don't have to run Tempest
+directly with you current working directory being the workspace, Tempest will
+take care of managing everything to be executed from there.
+
+Running from Anywhere
+---------------------
+Tempest run provides you with an option to execute tempest from anywhere on
+your system. You are required to provide a config file in this case with the
+``--config-file`` option. When run tempest will create a .testrepository
+directory and a .testr.conf file in your current working directory. This way
+you can use testr commands directly to inspect the state of the previous run.
+
Test Output
===========
By default tempest run's output to STDOUT will be generated using the
@@ -73,6 +91,8 @@
from oslo_log import log as logging
from testrepository.commands import run_argv
+from tempest.cmd import init
+from tempest.cmd import workspace
from tempest import config
@@ -82,7 +102,9 @@
class TempestRun(command.Command):
- def _set_env(self):
+ def _set_env(self, config_file=None):
+ if config_file:
+ CONF.set_config_path(os.path.abspath(config_file))
# NOTE(mtreinish): This is needed so that testr doesn't gobble up any
# stacktraces on failure.
if 'TESTR_PDB' in os.environ:
@@ -90,18 +112,45 @@
else:
os.environ["TESTR_PDB"] = ""
+ def _create_testrepository(self):
+ if not os.path.isdir('.testrepository'):
+ returncode = run_argv(['testr', 'init'], sys.stdin, sys.stdout,
+ sys.stderr)
+ if returncode:
+ sys.exit(returncode)
+
+ def _create_testr_conf(self):
+ top_level_path = os.path.dirname(os.path.dirname(__file__))
+ discover_path = os.path.join(top_level_path, 'test_discover')
+ file_contents = init.TESTR_CONF % (top_level_path, discover_path)
+ with open('.testr.conf', 'w+') as testr_conf_file:
+ testr_conf_file.write(file_contents)
+
def take_action(self, parsed_args):
- self._set_env()
returncode = 0
+ if parsed_args.config_file:
+ self._set_env(parsed_args.config_file)
+ else:
+ self._set_env()
+ # Workspace execution mode
+ if parsed_args.workspace:
+ workspace_mgr = workspace.WorkspaceManager(
+ parsed_args.workspace_path)
+ path = workspace_mgr.get_workspace(parsed_args.workspace)
+ os.chdir(path)
+ # NOTE(mtreinish): tempest init should create a .testrepository dir
+ # but since workspaces can be imported let's sanity check and
+ # ensure that one is created
+ self._create_testrepository()
# Local execution mode
- if os.path.isfile('.testr.conf'):
+ elif os.path.isfile('.testr.conf'):
# If you're running in local execution mode and there is not a
# testrepository dir create one
- if not os.path.isdir('.testrepository'):
- returncode = run_argv(['testr', 'init'], sys.stdin, sys.stdout,
- sys.stderr)
- if returncode:
- sys.exit(returncode)
+ self._create_testrepository()
+ # local execution with config file mode
+ elif parsed_args.config_file:
+ self._create_testr_conf()
+ self._create_testrepository()
else:
print("No .testr.conf file was found for local execution")
sys.exit(2)
@@ -124,6 +173,18 @@
return parser
def _add_args(self, parser):
+ # workspace args
+ parser.add_argument('--workspace', default=None,
+ help='Name of tempest workspace to use for running'
+ ' tests. You can see a list of workspaces '
+ 'with tempest workspace list')
+ parser.add_argument('--workspace-path', default=None,
+ dest='workspace_path',
+ help="The path to the workspace file, the default "
+ "is ~/.tempest/workspace.yaml")
+ # Configuration flags
+ parser.add_argument('--config-file', default=None, dest='config_file',
+ help='Configuration file to run tempest with')
# test selection args
regex = parser.add_mutually_exclusive_group()
regex.add_argument('--smoke', action='store_true',
diff --git a/tempest/cmd/subunit_describe_calls.py b/tempest/cmd/subunit_describe_calls.py
index c990add..da7f426 100644
--- a/tempest/cmd/subunit_describe_calls.py
+++ b/tempest/cmd/subunit_describe_calls.py
@@ -45,6 +45,7 @@
Ports file JSON structure
^^^^^^^^^^^^^^^^^^^^^^^^^
+::
{
"<port number>": "<name of service>",
@@ -54,6 +55,8 @@
Output file JSON structure
^^^^^^^^^^^^^^^^^^^^^^^^^^
+::
+
{
"full_test_name[with_id_and_tags]": [
{
diff --git a/tempest/common/fixed_network.py b/tempest/common/fixed_network.py
index 5f0685e..f57c18a 100644
--- a/tempest/common/fixed_network.py
+++ b/tempest/common/fixed_network.py
@@ -14,7 +14,7 @@
from oslo_log import log as logging
from tempest import exceptions
-from tempest.lib.common.utils import misc as misc_utils
+from tempest.lib.common.utils import test_utils
LOG = logging.getLogger(__name__)
@@ -31,7 +31,7 @@
list returns a 404, there are no found networks, or the found network
is invalid
"""
- caller = misc_utils.find_test_caller()
+ caller = test_utils.find_test_caller()
if not name:
raise exceptions.InvalidTestResource(type='network', name=name)
@@ -84,7 +84,7 @@
tenant network is available in the creds provider
:returns: a dict with 'id' and 'name' of the network
"""
- caller = misc_utils.find_test_caller()
+ caller = test_utils.find_test_caller()
net_creds = creds_provider.get_primary_creds()
network = getattr(net_creds, 'network', None)
if not network or not network.get('name'):
diff --git a/tempest/common/waiters.py b/tempest/common/waiters.py
index e083167..df08e30 100644
--- a/tempest/common/waiters.py
+++ b/tempest/common/waiters.py
@@ -18,7 +18,7 @@
from tempest.common import image as common_image
from tempest import config
from tempest import exceptions
-from tempest.lib.common.utils import misc as misc_utils
+from tempest.lib.common.utils import test_utils
from tempest.lib import exceptions as lib_exc
from tempest.lib.services.image.v1 import images_client as images_v1_client
@@ -91,7 +91,7 @@
'timeout': timeout})
message += ' Current status: %s.' % server_status
message += ' Current task state: %s.' % task_state
- caller = misc_utils.find_test_caller()
+ caller = test_utils.find_test_caller()
if caller:
message = '(%s) %s' % (caller, message)
raise exceptions.TimeoutException(message)
@@ -162,7 +162,7 @@
'status': status,
'current_status': current_status,
'timeout': client.build_timeout})
- caller = misc_utils.find_test_caller()
+ caller = test_utils.find_test_caller()
if caller:
message = '(%s) %s' % (caller, message)
raise exceptions.TimeoutException(message)
@@ -235,7 +235,7 @@
'status': status,
'timeout': client.build_timeout})
message += ' Current state of %s: %s.' % (attr, status_curr)
- caller = misc_utils.find_test_caller()
+ caller = test_utils.find_test_caller()
if caller:
message = '(%s) %s' % (caller, message)
raise exceptions.TimeoutException(message)
diff --git a/tempest/config.py b/tempest/config.py
index eb5e23a..8ec8b24 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -191,7 +191,12 @@
help="A list of enabled identity extensions with a special "
"entry all which indicates every extension is enabled. "
"Empty list indicates all extensions are disabled. "
- "To get the list of extensions run: 'keystone discover'")
+ "To get the list of extensions run: 'keystone discover'"),
+ # TODO(rodrigods): Remove the reseller flag when Kilo and Liberty is end
+ # of life.
+ cfg.BoolOpt('reseller',
+ default=False,
+ help='Does the environment support reseller?')
]
compute_group = cfg.OptGroup(name='compute',