Merge "Fix required projects for sanity plugin check job"
diff --git a/HACKING.rst b/HACKING.rst
index e767b25..e8791f8 100644
--- a/HACKING.rst
+++ b/HACKING.rst
@@ -35,6 +35,30 @@
- Clean up test data at the completion of each test
- Use configuration files for values that will vary by environment
+Supported OpenStack Components
+------------------------------
+
+Tempest's :ref:`library` and :ref:`plugin interface <tempest_plugin>` can be
+leveraged to support integration testing for virtually any OpenStack component.
+
+However, Tempest only offers **in-tree** integration testing coverage for the
+following components:
+
+* Cinder
+* Glance
+* Keystone
+* Neutron
+* Nova
+* Swift
+
+Historically, Tempest offered in-tree testing for other components as well, but
+since the introduction of the `External Plugin Interface`_, Tempest's in-tree
+testing scope has been limited to the projects above. Integration tests for
+projects not included above should go into one of the
+`relevant plugin projects`_.
+
+.. _External Plugin Interface: https://specs.openstack.org/openstack/qa-specs/specs/tempest/implemented/tempest-external-plugin-interface.html
+.. _relevant plugin projects: https://docs.openstack.org/tempest/latest/plugin-registry.html#detected-plugins
Exception Handling
------------------
@@ -431,7 +455,7 @@
by modifying Tempest's `lib installation script`_ for previous branches
(because DevStack is branched).
-.. _lib installation script: http://git.openstack.org/cgit/openstack-dev/devstack/tree/lib/tempest
+.. _lib installation script: https://git.openstack.org/cgit/openstack-dev/devstack/tree/lib/tempest
2. Bug fix on core project needing Tempest changes
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
diff --git a/README.rst b/README.rst
index 307ceb3..73930f1 100644
--- a/README.rst
+++ b/README.rst
@@ -165,7 +165,7 @@
interface and when Z is incremented it's a bug fix release for the library.
Also note that both Y and Z are reset to 0 at each increment of X.
-.. _semver: http://semver.org/
+.. _semver: https://semver.org/
Configuration
-------------
@@ -218,7 +218,7 @@
argument is no longer required, however it may perform faster if included.
For more information on these options and details about stestr, please see the
-`stestr documentation <http://stestr.readthedocs.io/en/latest/MANUAL.html>`_.
+`stestr documentation <https://stestr.readthedocs.io/en/latest/MANUAL.html>`_.
Python 3.x
----------
diff --git a/doc/source/plugin.rst b/doc/source/plugin.rst
index 9958792..dc0e94c 100644
--- a/doc/source/plugin.rst
+++ b/doc/source/plugin.rst
@@ -96,7 +96,7 @@
that users don't have to worry about inadvertently installing a Tempest plugin
when they install another package.
-.. _Branchless Tempest Spec: http://specs.openstack.org/openstack/qa-specs/specs/tempest/implemented/branchless-tempest.html
+.. _Branchless Tempest Spec: https://specs.openstack.org/openstack/qa-specs/specs/tempest/implemented/branchless-tempest.html
The sole advantage to integrating a plugin into an existing python project is
that it enables you to land code changes at the same time you land test changes
diff --git a/releasenotes/notes/bug-1799883-6ea95fc64f70c9ef.yaml b/releasenotes/notes/bug-1799883-6ea95fc64f70c9ef.yaml
new file mode 100644
index 0000000..630908f
--- /dev/null
+++ b/releasenotes/notes/bug-1799883-6ea95fc64f70c9ef.yaml
@@ -0,0 +1,7 @@
+---
+fixes:
+ - |
+ Fixed bug #1799883. ``tempest workspace register`` and ``tempest workspace move`` CLI
+ will now validate the value of the ``--path`` CLI argument and raise an exception if
+ it is None or empty string. Earlier both CLI actions were accepting None or empty path
+ which was confusing.
diff --git a/tempest/api/compute/servers/test_attach_interfaces.py b/tempest/api/compute/servers/test_attach_interfaces.py
index 2a5d607..05c2a28 100644
--- a/tempest/api/compute/servers/test_attach_interfaces.py
+++ b/tempest/api/compute/servers/test_attach_interfaces.py
@@ -24,6 +24,7 @@
from tempest.common import waiters
from tempest import config
from tempest.lib.common.utils.linux import remote_client
+from tempest.lib.common.utils import test_utils
from tempest.lib import decorators
from tempest.lib import exceptions as lib_exc
@@ -168,7 +169,9 @@
iface = self.interfaces_client.create_interface(
server['id'], net_id=network_id,
fixed_ips=fixed_ips)['interfaceAttachment']
- self.addCleanup(self.ports_client.delete_port, iface['port_id'])
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+ self.ports_client.delete_port,
+ iface['port_id'])
self._check_interface(iface, server_id=server['id'],
fixed_ip=ip_list[0])
return iface
@@ -260,16 +263,6 @@
interface_count = len(ifs)
self.assertGreater(interface_count, 0)
- try:
- iface = self._test_create_interface(server)
- except lib_exc.BadRequest as e:
- msg = ('Multiple possible networks found, use a Network ID to be '
- 'more specific.')
- if not CONF.compute.fixed_network_name and six.text_type(e) == msg:
- raise
- else:
- ifs.append(iface)
-
iface = self._test_create_interface_by_fixed_ips(server, ifs)
ifs.append(iface)
@@ -340,20 +333,61 @@
def test_add_remove_fixed_ip(self):
# Add and Remove the fixed IP to server.
server, ifs = self._create_server_get_interfaces()
- interface_count = len(ifs)
- self.assertGreater(interface_count, 0)
+ original_interface_count = len(ifs) # This is the number of ports.
+ self.assertGreater(original_interface_count, 0)
+ # Get the starting list of IPs on the server.
+ addresses = self.os_primary.servers_client.list_addresses(
+ server['id'])['addresses']
+ # There should be one entry for the single network mapped to a list of
+ # addresses, which at this point should have at least one entry.
+ # Note that we could start with two addresses depending on how tempest
+ # is configured for using floating IPs.
+ self.assertEqual(1, len(addresses), addresses) # number of networks
+ # Keep track of the original addresses so we can know which IP is new.
+ original_ips = [addr['addr'] for addr in list(addresses.values())[0]]
+ original_ip_count = len(original_ips)
+ self.assertGreater(original_ip_count, 0, addresses) # at least 1
network_id = ifs[0]['net_id']
+ # Add another fixed IP to the server. This should result in another
+ # fixed IP on the same network (and same port since we only have one
+ # port).
self.servers_client.add_fixed_ip(server['id'], networkId=network_id)
- # Remove the fixed IP from server.
+ # Wait for the ips count to increase by one.
+
+ def _wait_for_ip_increase():
+ _addresses = self.os_primary.servers_client.list_addresses(
+ server['id'])['addresses']
+ return len(list(_addresses.values())[0]) == original_ip_count + 1
+
+ if not test_utils.call_until_true(
+ _wait_for_ip_increase, CONF.compute.build_timeout,
+ CONF.compute.build_interval):
+ raise lib_exc.TimeoutException(
+ 'Timed out while waiting for IP count to increase.')
+
+ # Remove the fixed IP that we just added.
server_detail = self.os_primary.servers_client.show_server(
server['id'])['server']
# Get the Fixed IP from server.
fixed_ip = None
for ip_set in server_detail['addresses']:
for ip in server_detail['addresses'][ip_set]:
- if ip['OS-EXT-IPS:type'] == 'fixed':
+ if (ip['OS-EXT-IPS:type'] == 'fixed' and
+ ip['addr'] not in original_ips):
fixed_ip = ip['addr']
break
if fixed_ip is not None:
break
self.servers_client.remove_fixed_ip(server['id'], address=fixed_ip)
+ # Wait for the interface count to decrease by one.
+
+ def _wait_for_ip_decrease():
+ _addresses = self.os_primary.servers_client.list_addresses(
+ server['id'])['addresses']
+ return len(list(_addresses.values())[0]) == original_ip_count
+
+ if not test_utils.call_until_true(
+ _wait_for_ip_decrease, CONF.compute.build_timeout,
+ CONF.compute.build_interval):
+ raise lib_exc.TimeoutException(
+ 'Timed out while waiting for IP count to decrease.')
diff --git a/tempest/api/identity/admin/v2/test_services.py b/tempest/api/identity/admin/v2/test_services.py
index e2ed5ef..03543ac 100644
--- a/tempest/api/identity/admin/v2/test_services.py
+++ b/tempest/api/identity/admin/v2/test_services.py
@@ -89,14 +89,10 @@
service = self.services_client.create_service(
name=name, type=s_type,
description=description)['OS-KSADM:service']
+ self.addCleanup(self.services_client.delete_service, service['id'])
services.append(service)
service_ids = [svc['id'] for svc in services]
- def delete_services():
- for service_id in service_ids:
- self.services_client.delete_service(service_id)
-
- self.addCleanup(delete_services)
# List and Verify Services
body = self.services_client.list_services()['OS-KSADM:services']
found = [serv for serv in body if serv['id'] in service_ids]
diff --git a/tempest/api/identity/admin/v2/test_tenants.py b/tempest/api/identity/admin/v2/test_tenants.py
index cda721c..f68754e 100644
--- a/tempest/api/identity/admin/v2/test_tenants.py
+++ b/tempest/api/identity/admin/v2/test_tenants.py
@@ -50,7 +50,7 @@
'been sent in response for create')
body = self.tenants_client.show_tenant(tenant_id)['tenant']
desc2 = body['description']
- self.assertEqual(desc2, tenant_desc, 'Description does not appear'
+ self.assertEqual(desc2, tenant_desc, 'Description does not appear '
'to be set')
self.tenants_client.delete_tenant(tenant_id)
diff --git a/tempest/api/identity/admin/v3/test_projects.py b/tempest/api/identity/admin/v3/test_projects.py
index 6ddf42e..f75edaa 100644
--- a/tempest/api/identity/admin/v3/test_projects.py
+++ b/tempest/api/identity/admin/v3/test_projects.py
@@ -31,7 +31,7 @@
'been sent in response for create')
body = self.projects_client.show_project(project_id)['project']
desc2 = body['description']
- self.assertEqual(desc2, project_desc, 'Description does not appear'
+ self.assertEqual(desc2, project_desc, 'Description does not appear '
'to be set')
@decorators.idempotent_id('5f50fe07-8166-430b-a882-3b2ee0abe26f')
diff --git a/tempest/api/identity/admin/v3/test_trusts.py b/tempest/api/identity/admin/v3/test_trusts.py
index 2530072..83b3c30 100644
--- a/tempest/api/identity/admin/v3/test_trusts.py
+++ b/tempest/api/identity/admin/v3/test_trusts.py
@@ -39,7 +39,6 @@
# Use alt_username as the trustee
self.trust_id = None
self.create_trustor_and_roles()
- self.addCleanup(self.cleanup_user_and_roles)
def tearDown(self):
if self.trust_id:
@@ -55,6 +54,7 @@
trustor_project_name,
domain_id=CONF.identity.default_domain_id)['project']
self.trustor_project_id = project['id']
+ self.addCleanup(self.projects_client.delete_project, project['id'])
self.assertIsNotNone(self.trustor_project_id)
# Create a trustor User
@@ -69,6 +69,7 @@
email=u_email,
project_id=self.trustor_project_id,
domain_id=CONF.identity.default_domain_id)['user']
+ self.addCleanup(self.users_client.delete_user, user['id'])
self.trustor_user_id = user['id']
# And two roles, one we'll delegate and one we won't
@@ -76,10 +77,12 @@
self.not_delegated_role = data_utils.rand_name('NotDelegatedRole')
role = self.roles_client.create_role(name=self.delegated_role)['role']
+ self.addCleanup(self.roles_client.delete_role, role['id'])
self.delegated_role_id = role['id']
role = self.roles_client.create_role(
name=self.not_delegated_role)['role']
+ self.addCleanup(self.roles_client.delete_role, role['id'])
self.not_delegated_role_id = role['id']
# Assign roles to trustor
@@ -109,16 +112,6 @@
os = clients.Manager(credentials=creds)
self.trustor_client = os.trusts_client
- def cleanup_user_and_roles(self):
- if self.trustor_user_id:
- self.users_client.delete_user(self.trustor_user_id)
- if self.trustor_project_id:
- self.projects_client.delete_project(self.trustor_project_id)
- if self.delegated_role_id:
- self.roles_client.delete_role(self.delegated_role_id)
- if self.not_delegated_role_id:
- self.roles_client.delete_role(self.not_delegated_role_id)
-
def create_trust(self, impersonate=True, expires=None):
trust_create = self.trustor_client.create_trust(
diff --git a/tempest/api/identity/v2/test_ec2_credentials.py b/tempest/api/identity/v2/test_ec2_credentials.py
index 237e728..9981ef8 100644
--- a/tempest/api/identity/v2/test_ec2_credentials.py
+++ b/tempest/api/identity/v2/test_ec2_credentials.py
@@ -57,18 +57,19 @@
self.creds.user_id,
tenant_id=self.creds.tenant_id)["credential"]
created_creds.append(creds1['access'])
+ self.addCleanup(
+ self.non_admin_users_client.delete_user_ec2_credential,
+ self.creds.user_id, creds1['access'])
+
# create second ec2 credentials
creds2 = self.non_admin_users_client.create_user_ec2_credential(
self.creds.user_id,
tenant_id=self.creds.tenant_id)["credential"]
created_creds.append(creds2['access'])
- # add credentials to be cleaned up
- self.addCleanup(
- self.non_admin_users_client.delete_user_ec2_credential,
- self.creds.user_id, creds1['access'])
self.addCleanup(
self.non_admin_users_client.delete_user_ec2_credential,
self.creds.user_id, creds2['access'])
+
# get the list of user ec2 credentials
resp = self.non_admin_users_client.list_user_ec2_credentials(
self.creds.user_id)["credentials"]
diff --git a/tempest/api/network/test_dhcp_ipv6.py b/tempest/api/network/test_dhcp_ipv6.py
index 0730d58..399954c 100644
--- a/tempest/api/network/test_dhcp_ipv6.py
+++ b/tempest/api/network/test_dhcp_ipv6.py
@@ -135,7 +135,7 @@
real_ip, eui_ip = self._get_ips_from_subnet(**kwargs)
self._clean_network()
self.assertEqual(eui_ip, real_ip,
- ('Real port IP %s shall be equal to EUI-64 %s'
+ ('Real port IP %s shall be equal to EUI-64 %s '
'when ipv6_ra_mode=%s,ipv6_address_mode=%s') % (
real_ip, eui_ip,
ra_mode if ra_mode else "Off",
diff --git a/tempest/cmd/cleanup.py b/tempest/cmd/cleanup.py
index 29abd49..2f54f9a 100644
--- a/tempest/cmd/cleanup.py
+++ b/tempest/cmd/cleanup.py
@@ -279,7 +279,7 @@
self.admin_id,
self.admin_role_id)
except Exception as ex:
- LOG.exception("Failed removing role from project which still"
+ LOG.exception("Failed removing role from project which still "
"exists, exception: %s", ex)
def _project_exists(self, project_id):
diff --git a/tempest/cmd/run.py b/tempest/cmd/run.py
index 84c6d9a..3e84b82 100644
--- a/tempest/cmd/run.py
+++ b/tempest/cmd/run.py
@@ -243,8 +243,8 @@
parser.add_argument('--load-list', '--load_list',
help='Path to a non-regex whitelist file, '
'this file contains a separate test '
- 'on each newline. This command'
- 'supports files created by the tempest'
+ 'on each newline. This command '
+ 'supports files created by the tempest '
'run ``--list-tests`` command')
# list only args
parser.add_argument('--list-tests', '-l', action='store_true',
diff --git a/tempest/cmd/workspace.py b/tempest/cmd/workspace.py
index d276bde..d0c4b28 100644
--- a/tempest/cmd/workspace.py
+++ b/tempest/cmd/workspace.py
@@ -94,7 +94,7 @@
@lockutils.synchronized('workspaces', external=True)
def move_workspace(self, name, path):
self._populate()
- path = os.path.abspath(os.path.expanduser(path))
+ path = os.path.abspath(os.path.expanduser(path)) if path else path
self._name_exists(name)
self._validate_path(path)
self.workspaces[name] = path
@@ -115,6 +115,7 @@
@lockutils.synchronized('workspaces', external=True)
def remove_workspace_directory(self, workspace_path):
+ self._validate_path(workspace_path)
shutil.rmtree(workspace_path)
@lockutils.synchronized('workspaces', external=True)
@@ -136,6 +137,10 @@
sys.exit(1)
def _validate_path(self, path):
+ if not path:
+ print("None or empty path is specified for workspace."
+ " Please specify correct workspace path.")
+ sys.exit(1)
if not os.path.exists(path):
print("Path does not exist.")
sys.exit(1)
@@ -144,7 +149,7 @@
def register_new_workspace(self, name, path, init=False):
"""Adds the new workspace and writes out the new workspace config"""
self._populate()
- path = os.path.abspath(os.path.expanduser(path))
+ path = os.path.abspath(os.path.expanduser(path)) if path else path
# This only happens when register is called from outside of init
if not init:
self._validate_path(path)
diff --git a/tempest/config.py b/tempest/config.py
index 6c6ff58..18c9d9d 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -170,11 +170,22 @@
cfg.IntOpt('user_lockout_failure_attempts',
default=2,
help="The number of unsuccessful login attempts the user is "
- "allowed before having the account locked."),
+ "allowed before having the account locked. This only "
+ "takes effect when identity-feature-enabled."
+ "security_compliance is set to 'True'. For more details, "
+ "refer to keystone config options keystone.conf:"
+ "security_compliance.lockout_failure_attempts. "
+ "This feature is disabled by default in keystone."),
cfg.IntOpt('user_lockout_duration',
default=5,
help="The number of seconds a user account will remain "
- "locked."),
+ "locked. This only takes "
+ "effect when identity-feature-enabled.security_compliance "
+ "is set to 'True'. For more details, refer to "
+ "keystone config options "
+ "keystone.conf:security_compliance.lockout_duration. "
+ "Setting this option will have no effect unless you also "
+ "set identity.user_lockout_failure_attempts."),
cfg.IntOpt('user_unique_last_password_count',
default=2,
help="The number of passwords for a user that must be unique "
diff --git a/tempest/lib/auth.py b/tempest/lib/auth.py
index 2dd9d00..8e6d3d5 100644
--- a/tempest/lib/auth.py
+++ b/tempest/lib/auth.py
@@ -324,7 +324,7 @@
pass
if expiry is None:
raise ValueError(
- "time data '{data}' does not match any of the"
+ "time data '{data}' does not match any of the "
"expected formats: {formats}".format(
data=expiry_string, formats=self.EXPIRY_DATE_FORMATS))
return expiry
diff --git a/tempest/lib/services/volume/v3/backups_client.py b/tempest/lib/services/volume/v3/backups_client.py
index f2d2d21..fb64333 100644
--- a/tempest/lib/services/volume/v3/backups_client.py
+++ b/tempest/lib/services/volume/v3/backups_client.py
@@ -104,7 +104,12 @@
return rest_client.ResponseBody(resp, body)
def import_backup(self, **kwargs):
- """Import backup metadata record."""
+ """Import backup metadata record.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/block-storage/v3/index.html#import-a-backup
+ """
post_body = json.dumps({'backup-record': kwargs})
resp, body = self.post("backups/import_record", post_body)
body = json.loads(body)
diff --git a/tempest/test.py b/tempest/test.py
index f2babbb..3e98c33 100644
--- a/tempest/test.py
+++ b/tempest/test.py
@@ -581,7 +581,7 @@
def setUp(self):
super(BaseTestCase, self).setUp()
if not self.__setupclass_called:
- raise RuntimeError("setUpClass does not calls the super's"
+ raise RuntimeError("setUpClass does not calls the super's "
"setUpClass in the " +
self.__class__.__name__)
at_exit_set.add(self.__class__)
diff --git a/tempest/tests/cmd/test_workspace.py b/tempest/tests/cmd/test_workspace.py
index a256368..65481de 100644
--- a/tempest/tests/cmd/test_workspace.py
+++ b/tempest/tests/cmd/test_workspace.py
@@ -140,6 +140,17 @@
self.assertEqual(
self.workspace_manager.get_workspace(self.name), new_path)
+ def test_workspace_manager_move_no_workspace_path(self):
+ new_path = ""
+ with patch('sys.stdout', new_callable=StringIO) as mock_stdout:
+ ex = self.assertRaises(SystemExit,
+ self.workspace_manager.move_workspace,
+ self.name, new_path)
+ self.assertEqual(1, ex.code)
+ self.assertEqual(mock_stdout.getvalue(),
+ "None or empty path is specified for workspace."
+ " Please specify correct workspace path.\n")
+
def test_workspace_manager_remove_entry(self):
self.workspace_manager.remove_workspace_entry(self.name)
self.assertIsNone(self.workspace_manager.get_workspace(self.name))
@@ -149,6 +160,18 @@
self.workspace_manager.remove_workspace_directory(path)
self.assertIsNone(self.workspace_manager.get_workspace(self.name))
+ def test_workspace_manager_remove_directory_no_path(self):
+ no_path = ""
+ with patch('sys.stdout', new_callable=StringIO) as mock_stdout:
+ ex = self.assertRaises(SystemExit,
+ self.workspace_manager.
+ remove_workspace_directory,
+ no_path)
+ self.assertEqual(1, ex.code)
+ self.assertEqual(mock_stdout.getvalue(),
+ "None or empty path is specified for workspace."
+ " Please specify correct workspace path.\n")
+
def test_path_expansion(self):
name = data_utils.rand_uuid()
path = os.path.join("~", name)
@@ -207,3 +230,15 @@
self.assertEqual(mock_stdout.getvalue(),
"None or empty name is specified."
" Please specify correct name for workspace.\n")
+
+ def test_register_new_workspace_no_path(self):
+ no_path = ""
+ with patch('sys.stdout', new_callable=StringIO) as mock_stdout:
+ ex = self.assertRaises(SystemExit,
+ self.workspace_manager.
+ register_new_workspace,
+ self.name, no_path)
+ self.assertEqual(1, ex.code)
+ self.assertEqual(mock_stdout.getvalue(),
+ "None or empty path is specified for workspace."
+ " Please specify correct workspace path.\n")
diff --git a/tools/generate-tempest-plugins-list.py b/tools/generate-tempest-plugins-list.py
index 4eb78fb..3772774 100644
--- a/tools/generate-tempest-plugins-list.py
+++ b/tools/generate-tempest-plugins-list.py
@@ -74,7 +74,8 @@
# Gerrit prepends 4 garbage octets to the JSON, in order to counter
# cross-site scripting attacks. Therefore we must discard it so the
# json library won't choke.
-projects = sorted(filter(is_in_openstack_namespace, json.loads(r.read()[4:])))
+content = r.read().decode('utf-8')[4:]
+projects = sorted(filter(is_in_openstack_namespace, json.loads(content)))
# Retrieve projects having no deb, puppet, ui or spec namespace as those
# namespaces do not contains tempest plugins.
diff --git a/tools/generate-tempest-plugins-list.sh b/tools/generate-tempest-plugins-list.sh
index b27b23a..111c9ce 100755
--- a/tools/generate-tempest-plugins-list.sh
+++ b/tools/generate-tempest-plugins-list.sh
@@ -41,8 +41,6 @@
set -ex
(
-declare -A plugins
-
if [[ -r doc/source/data/tempest-plugins-registry.header ]]; then
cat doc/source/data/tempest-plugins-registry.header
fi