Merge "Set max_microversion 2.35 for nova security group tests"
diff --git a/.zuul.yaml b/.zuul.yaml
index e5e093a..8dcb935 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -45,6 +45,19 @@
ENABLE_FILE_INJECTION: true
- job:
+ name: tempest-full-parallel
+ parent: tempest-full
+ voting: false
+ branches:
+ - master
+ description: |
+ Base integration test with Neutron networking and py27.
+ It includes all scenarios as it was in the past.
+ This job runs all scenario tests in parallel!
+ vars:
+ tox_envlist: full-parallel
+
+- job:
name: tempest-full-py3
parent: devstack-tempest
branches:
@@ -68,6 +81,26 @@
# without Swift, c-bak cannot run (in the Gate at least)
c-bak: false
+- nodeset:
+ name: openstack-bionic-node
+ nodes:
+ - name: controller
+ label: ubuntu-bionic
+ groups:
+ - name: tempest
+ nodes:
+ - controller
+
+- job:
+ name: tempest-full-py36
+ parent: tempest-full-py3
+ nodeset: openstack-bionic-node
+ branches:
+ - master
+ description: |
+ Base integration test with Neutron networking and py36.
+ voting: false
+
- job:
name: tempest-full-queens
parent: tempest-full
@@ -175,6 +208,26 @@
- ^roles/
- ^.zuul.yaml$
- nova-multiattach
+ - tempest-full-parallel:
+ irrelevant-files:
+ - ^(test-|)requirements.txt$
+ - ^.*\.rst$
+ - ^doc/.*$
+ - ^etc/.*$
+ - ^releasenotes/.*$
+ - ^setup.cfg$
+ - ^tempest/hacking/.*$
+ - ^tempest/tests/.*$
+ - tempest-full-py36:
+ irrelevant-files:
+ - ^(test-|)requirements.txt$
+ - ^.*\.rst$
+ - ^doc/.*$
+ - ^etc/.*$
+ - ^releasenotes/.*$
+ - ^setup.cfg$
+ - ^tempest/hacking/.*$
+ - ^tempest/tests/.*$
- tempest-full-queens:
irrelevant-files:
- ^(test-|)requirements.txt$
@@ -221,6 +274,16 @@
- ^setup.cfg$
- ^tempest/hacking/.*$
- ^tempest/tests/.*$
+ - nova-live-migration:
+ irrelevant-files:
+ - ^(test-|)requirements.txt$
+ - ^.*\.rst$
+ - ^doc/.*$
+ - ^etc/.*$
+ - ^releasenotes/.*$
+ - ^setup.cfg$
+ - ^tempest/hacking/.*$
+ - ^tempest/tests/.*$
periodic-stable:
jobs:
- tempest-full-queens
diff --git a/HACKING.rst b/HACKING.rst
index f961884..bb55ac5 100644
--- a/HACKING.rst
+++ b/HACKING.rst
@@ -363,13 +363,24 @@
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
When adding tests for new features that were not in previous releases of the
-projects the new test has to be properly skipped with a feature flag. Whether
-this is just as simple as using the @utils.requires_ext() decorator to
-check if the required extension (or discoverable optional API) is enabled or
+projects the new test has to be properly skipped with a feature flag. This can
+be just as simple as using the ``@utils.requires_ext()`` or
+``testtools.skipUnless`` decorators to check if the required extension (or
+discoverable optional API) or feature is enabled or can be as difficult as
adding a new config option to the appropriate section. If there isn't a method
of selecting the new **feature** from the config file then there won't be a
-mechanism to disable the test with older stable releases and the new test won't
-be able to merge.
+mechanism to disable the test with older stable releases and the new test
+won't be able to merge.
+
+Introduction of a new feature flag requires specifying a default value for
+the corresponding config option that is appropriate in the latest OpenStack
+release. Because Tempest is branchless, the feature flag's default value will
+need to be overridden to a value that is appropriate in earlier releases
+in which the feature isn't available. In DevStack, this can be accomplished
+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
2. Bug fix on core project needing Tempest changes
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
diff --git a/README.rst b/README.rst
index f8059ae..044ae09 100644
--- a/README.rst
+++ b/README.rst
@@ -15,7 +15,7 @@
This is a set of integration tests to be run against a live OpenStack
cluster. Tempest has batteries of tests for OpenStack API validation,
-Scenarios, and other specific tests useful in validating an OpenStack
+scenarios, and other specific tests useful in validating an OpenStack
deployment.
Design Principles
diff --git a/REVIEWING.rst b/REVIEWING.rst
index 766d0c6..a880181 100644
--- a/REVIEWING.rst
+++ b/REVIEWING.rst
@@ -42,8 +42,8 @@
API Stability
-------------
-Tests should only be added for a published stable APIs. If a patch contains
-tests for an API which hasn't been marked as stable or for an API that which
+Tests should only be added for published stable APIs. If a patch contains
+tests for an API which hasn't been marked as stable or for an API which
doesn't conform to the `API stability guidelines
<https://wiki.openstack.org/wiki/Governance/Approved/APIStability>`_ then it
should not be approved.
diff --git a/doc/source/microversion_testing.rst b/doc/source/microversion_testing.rst
index 942f969..3bc1d0c 100644
--- a/doc/source/microversion_testing.rst
+++ b/doc/source/microversion_testing.rst
@@ -306,10 +306,18 @@
.. _2.6: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id5
+ * `2.9`_
+
+ .. _2.9: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id8
+
* `2.10`_
.. _2.10: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id9
+ * `2.19`_
+
+ .. _2.19: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id17
+
* `2.20`_
.. _2.20: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id18
@@ -322,6 +330,10 @@
.. _2.25: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#maximum-in-mitaka
+ * `2.26`_
+
+ .. _2.26: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id23
+
* `2.32`_
.. _2.32: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id29
@@ -342,6 +354,14 @@
.. _2.48: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id43
+ * `2.54`_
+
+ .. _2.54: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id4
+
+ * `2.55`_
+
+ .. _2.55: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id49
+
* `2.60`_
.. _2.60: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id54
diff --git a/releasenotes/notes/add-update-flavor--api-to-flavors-client-a859542fe54aab7c.yaml b/releasenotes/notes/add-update-flavor--api-to-flavors-client-a859542fe54aab7c.yaml
new file mode 100644
index 0000000..222a99f
--- /dev/null
+++ b/releasenotes/notes/add-update-flavor--api-to-flavors-client-a859542fe54aab7c.yaml
@@ -0,0 +1,4 @@
+---
+features:
+ - |
+ Add update flavor API to compute flavors_client library.
diff --git a/releasenotes/notes/bp-application-credentials-df69b1f617db1bb9.yaml b/releasenotes/notes/bp-application-credentials-df69b1f617db1bb9.yaml
new file mode 100644
index 0000000..53125ef
--- /dev/null
+++ b/releasenotes/notes/bp-application-credentials-df69b1f617db1bb9.yaml
@@ -0,0 +1,9 @@
+---
+features:
+ - |
+ [`blueprint application-credentials <https://blueprints.launchpad.net/keystone/+spec/application-credentials>`_]
+ Tempest can test keystone's application credentials interface. A new client
+ library is added for application credentials, and a new config option,
+ ``[identity-feature-enabled]/application_credentials``, can control whether
+ the application credentials feature is tested (defaults to False,
+ indicating the feature is not enabled in the cloud under test).
diff --git a/releasenotes/notes/identity-v3-project-tags-client-36683c6a8644e54b.yaml b/releasenotes/notes/identity-v3-project-tags-client-36683c6a8644e54b.yaml
new file mode 100644
index 0000000..dfbcc7d
--- /dev/null
+++ b/releasenotes/notes/identity-v3-project-tags-client-36683c6a8644e54b.yaml
@@ -0,0 +1,12 @@
+---
+features:
+ - |
+ Add ``project_tags_client`` to the identity v3 library. This feature
+ enables the possibility of invoking the following API actions:
+
+ * update_project_tag
+ * list_project_tags
+ * update_all_project_tags
+ * check_project_tag_existence
+ * delete_project_tag
+ * delete_all_project_tags
diff --git a/releasenotes/notes/vnc-hardcoded-server-name-removed-6f8d1e90a175dc08.yaml b/releasenotes/notes/vnc-hardcoded-server-name-removed-6f8d1e90a175dc08.yaml
new file mode 100644
index 0000000..0da2ddc
--- /dev/null
+++ b/releasenotes/notes/vnc-hardcoded-server-name-removed-6f8d1e90a175dc08.yaml
@@ -0,0 +1,10 @@
+---
+features:
+ - |
+ New string configuration option ``vnc_server_header`` is added
+ to ``compute-feature-enabled`` section. It offers to provide VNC server
+ name that is to be expected in the responce header. For example, obvious
+ at hand names is 'WebSockify', 'nginx'.
+fixes:
+ - |
+ Fix VNC server response header issue when it is behind reverse proxy
diff --git a/releasenotes/notes/volume-service-testing-default-to-v3-endpoints-20b86895a590925d.yaml b/releasenotes/notes/volume-service-testing-default-to-v3-endpoints-20b86895a590925d.yaml
new file mode 100644
index 0000000..ea69293
--- /dev/null
+++ b/releasenotes/notes/volume-service-testing-default-to-v3-endpoints-20b86895a590925d.yaml
@@ -0,0 +1,8 @@
+---
+upgrade:
+ - |
+ The volume config option ``catalog_type`` default is changed to
+ ``volumev3`` which is v3 API endpoint configured in devstack.
+ With this change Tempest will be testing v3 API as default.
+ User who want to test v2 API can still test by configuring the
+ ``catalog_type`` to v2 endpoint.
diff --git a/releasenotes/notes/volume-v3-service-clients-a863a6336af56cca.yaml b/releasenotes/notes/volume-v3-service-clients-a863a6336af56cca.yaml
new file mode 100644
index 0000000..b572a34
--- /dev/null
+++ b/releasenotes/notes/volume-v3-service-clients-a863a6336af56cca.yaml
@@ -0,0 +1,12 @@
+---
+features:
+ - |
+ Adds volume service clients for v3 APIs. As v3 base API should be
+ identical to v2 APIs, we just copy all existing v2 service client
+ for v3 API.
+deprecations:
+ - |
+ Deprecates the volume service clients for v2 APIs. Volume v2 APIs
+ are deprecated in all supported stable branches, so it's time
+ to deprecate the tempest service clients for v2 APIs and remove in future
+ release.
diff --git a/requirements.txt b/requirements.txt
index 38f4a2a..7520d42 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -7,7 +7,7 @@
testtools>=2.2.0 # MIT
paramiko>=2.0.0 # LGPLv2.1+
netaddr>=0.7.18 # BSD
-oslo.concurrency>=3.25.0 # Apache-2.0
+oslo.concurrency>=3.26.0 # Apache-2.0
oslo.config>=5.2.0 # Apache-2.0
oslo.log>=3.36.0 # Apache-2.0
stestr>=1.0.0 # Apache-2.0
diff --git a/setup.cfg b/setup.cfg
index c981370..96ee7ea 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -47,12 +47,5 @@
oslo.config.opts =
tempest.config = tempest.config:list_opts
-[build_sphinx]
-all-files = 1
-# warning can be generated by using GENERATE_TEMPEST_PLUGIN_LIST='False'
-warning-is-error = 0
-build-dir = doc/build
-source-dir = doc/source
-
[wheel]
universal = 1
diff --git a/tempest/README.rst b/tempest/README.rst
index 62821de..a5f4a92 100644
--- a/tempest/README.rst
+++ b/tempest/README.rst
@@ -12,12 +12,12 @@
and guidelines. Below is the overview of the Tempest respository structure
to make this clear.
- .. code-block:: console
+.. code-block:: console
- tempest/
- api/ - API tests
- scenario/ - complex scenario tests
- tests/ - unit tests for Tempest internals
+ tempest/
+ api/ - API tests
+ scenario/ - complex scenario tests
+ tests/ - unit tests for Tempest internals
Each of these directories contains different types of tests. What
belongs in each directory, the rules and examples for good tests, are
diff --git a/tempest/api/compute/admin/test_aggregates_negative.py b/tempest/api/compute/admin/test_aggregates_negative.py
index 6df8410..a6e0efa 100644
--- a/tempest/api/compute/admin/test_aggregates_negative.py
+++ b/tempest/api/compute/admin/test_aggregates_negative.py
@@ -27,15 +27,16 @@
def setup_clients(cls):
super(AggregatesAdminNegativeTestJSON, cls).setup_clients()
cls.client = cls.os_admin.aggregates_client
- cls.hyper_client = cls.os_admin.hypervisor_client
+ cls.services_client = cls.os_admin.services_client
@classmethod
def resource_setup(cls):
super(AggregatesAdminNegativeTestJSON, cls).resource_setup()
cls.aggregate_name_prefix = 'test_aggregate'
- hyper_list = cls.hyper_client.list_hypervisors()['hypervisors']
- cls.hosts = [v['hypervisor_hostname'] for v in hyper_list
+ svc_list = cls.services_client.list_services(
+ binary='nova-compute')['services']
+ cls.hosts = [v['host'] for v in svc_list
if v['status'] == 'enabled' and v['state'] == 'up']
def _create_test_aggregate(self):
diff --git a/tempest/api/compute/admin/test_flavors_microversions.py b/tempest/api/compute/admin/test_flavors_microversions.py
new file mode 100644
index 0000000..027af25
--- /dev/null
+++ b/tempest/api/compute/admin/test_flavors_microversions.py
@@ -0,0 +1,43 @@
+# Copyright 2018 NEC Corporation.
+# 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.compute import base
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+
+
+class FlavorsV255TestJSON(base.BaseV2ComputeAdminTest):
+ min_microversion = '2.55'
+ max_microversion = 'latest'
+
+ # NOTE(gmann): This class tests the flavors APIs
+ # response schema for the 2.55 microversion.
+
+ @decorators.idempotent_id('61976b25-488d-41dc-9dcb-cb9693a7b075')
+ def test_crud_flavor(self):
+ flavor_id = data_utils.rand_int_id(start=1000)
+ # Checking create API response schema
+ new_flavor_id = self.create_flavor(ram=512,
+ vcpus=1,
+ disk=10,
+ id=flavor_id)['id']
+ # Checking show API response schema
+ self.flavors_client.show_flavor(new_flavor_id)['flavor']
+ # Checking update API response schema
+ self.admin_flavors_client.update_flavor(new_flavor_id,
+ description='new')['flavor']
+ # Checking list details API response schema
+ self.flavors_client.list_flavors(detail=True)['flavors']
+ # Checking list API response schema
+ self.flavors_client.list_flavors()['flavors']
diff --git a/tempest/api/compute/admin/test_floating_ips_bulk.py b/tempest/api/compute/admin/test_floating_ips_bulk.py
index ba19937..72d09ed 100644
--- a/tempest/api/compute/admin/test_floating_ips_bulk.py
+++ b/tempest/api/compute/admin/test_floating_ips_bulk.py
@@ -31,6 +31,7 @@
API documentation - http://docs.openstack.org/api/openstack-compute/2/
content/ext-os-floating-ips-bulk.html
"""
+ max_microversion = '2.35'
@classmethod
def setup_clients(cls):
diff --git a/tempest/api/compute/admin/test_live_migration.py b/tempest/api/compute/admin/test_live_migration.py
index 2398cf1..bc38144 100644
--- a/tempest/api/compute/admin/test_live_migration.py
+++ b/tempest/api/compute/admin/test_live_migration.py
@@ -147,6 +147,7 @@
@testtools.skipIf(not CONF.compute_feature_enabled.
block_migrate_cinder_iscsi,
'Block Live migration not configured for iSCSI')
+ @utils.services('volume')
def test_iscsi_volume(self):
server = self.create_test_server(wait_until="ACTIVE")
server_id = server['id']
diff --git a/tempest/api/compute/admin/test_servers.py b/tempest/api/compute/admin/test_servers.py
index 3f06c4e..cdfc44a 100644
--- a/tempest/api/compute/admin/test_servers.py
+++ b/tempest/api/compute/admin/test_servers.py
@@ -16,6 +16,7 @@
from tempest.common import waiters
from tempest.lib.common.utils import data_utils
from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
class ServersAdminTestJSON(base.BaseV2ComputeAdminTest):
@@ -61,9 +62,13 @@
@decorators.idempotent_id('d56e9540-73ed-45e0-9b88-98fc419087eb')
def test_list_servers_detailed_filter_by_invalid_status(self):
params = {'status': 'invalid_status'}
- body = self.client.list_servers(detail=True, **params)
- servers = body['servers']
- self.assertEmpty(servers)
+ if self.is_requested_microversion_compatible('2.37'):
+ body = self.client.list_servers(detail=True, **params)
+ servers = body['servers']
+ self.assertEmpty(servers)
+ else:
+ self.assertRaises(lib_exc.BadRequest, self.client.list_servers,
+ detail=True, **params)
@decorators.idempotent_id('51717b38-bdc1-458b-b636-1cf82d99f62f')
def test_list_servers_by_admin(self):
diff --git a/tempest/api/compute/admin/test_services.py b/tempest/api/compute/admin/test_services.py
index f3eb597..73e191b 100644
--- a/tempest/api/compute/admin/test_services.py
+++ b/tempest/api/compute/admin/test_services.py
@@ -56,15 +56,3 @@
# sort the lists before comparing, to take out dependency
# on order.
self.assertEqual(sorted(s1), sorted(s2))
-
- @decorators.idempotent_id('39397f6f-37b8-4234-8671-281e44c74025')
- def test_get_service_by_service_and_host_name(self):
- services = self.client.list_services()['services']
- host_name = services[0]['host']
- binary_name = services[0]['binary']
-
- services = self.client.list_services(host=host_name,
- binary=binary_name)['services']
- self.assertEqual(1, len(services))
- self.assertEqual(host_name, services[0]['host'])
- self.assertEqual(binary_name, services[0]['binary'])
diff --git a/tempest/api/compute/base.py b/tempest/api/compute/base.py
index 9759be7..975728c 100644
--- a/tempest/api/compute/base.py
+++ b/tempest/api/compute/base.py
@@ -127,6 +127,36 @@
cls.image_ssh_password = CONF.validation.image_ssh_password
@classmethod
+ def is_requested_microversion_compatible(cls, max_version):
+ """Check the compatibility of selected request microversion
+
+ This method will check if selected request microversion
+ (cls.request_microversion) for test is compatible with respect
+ to 'max_version'. Compatible means if selected request microversion
+ is in the range(<=) of 'max_version'.
+
+ :param max_version: maximum microversion to compare for compatibility.
+ Example: '2.30'
+ :returns: True if selected request microversion is compatible with
+ 'max_version'. False in other case.
+ """
+ try:
+ req_version_obj = api_version_request.APIVersionRequest(
+ cls.request_microversion)
+ # NOTE(gmann): This is case where this method is used before calling
+ # resource_setup(), where cls.request_microversion is set. There may
+ # not be any such case but still we can handle this case.
+ except AttributeError:
+ request_microversion = (
+ api_version_utils.select_request_microversion(
+ cls.min_microversion,
+ CONF.compute.min_microversion))
+ req_version_obj = api_version_request.APIVersionRequest(
+ request_microversion)
+ max_version_obj = api_version_request.APIVersionRequest(max_version)
+ return req_version_obj <= max_version_obj
+
+ @classmethod
def server_check_teardown(cls):
"""Checks is the shared server clean enough for subsequent test.
@@ -532,10 +562,10 @@
def get_host_other_than(self, server_id):
source_host = self.get_host_for_server(server_id)
- hypers = self.os_admin.hypervisor_client.list_hypervisors(
- )['hypervisors']
- hosts = [hyper['hypervisor_hostname'] for hyper in hypers
- if hyper['state'] == 'up' and hyper['status'] == 'enabled']
+ svcs = self.os_admin.services_client.list_services(
+ binary='nova-compute')['services']
+ hosts = [svc['host'] for svc in svcs
+ if svc['state'] == 'up' and svc['status'] == 'enabled']
for target_host in hosts:
if source_host != target_host:
diff --git a/tempest/api/compute/servers/test_create_server.py b/tempest/api/compute/servers/test_create_server.py
index c660821..122c4f5 100644
--- a/tempest/api/compute/servers/test_create_server.py
+++ b/tempest/api/compute/servers/test_create_server.py
@@ -135,8 +135,13 @@
servers_client=self.client)
hostname = linux_client.exec_command("hostname").rstrip()
msg = ('Failed while verifying servername equals hostname. Expected '
- 'hostname "%s" but got "%s".' % (self.name, hostname))
- self.assertEqual(self.name.lower(), hostname, msg)
+ 'hostname "%s" but got "%s".' %
+ (self.name, hostname.split(".")[0]))
+ # NOTE(zhufl): Some images will add postfix for the hostname, e.g.,
+ # if hostname is "aaa", postfix ".novalocal" may be added to it, and
+ # the hostname will be "aaa.novalocal" then, so we should ignore the
+ # postfix when checking whether hostname equals self.name.
+ self.assertEqual(self.name.lower(), hostname.split(".")[0], msg)
class ServersTestManualDisk(ServersTestJSON):
diff --git a/tempest/api/compute/servers/test_device_tagging.py b/tempest/api/compute/servers/test_device_tagging.py
index 43046ca..1213a04 100644
--- a/tempest/api/compute/servers/test_device_tagging.py
+++ b/tempest/api/compute/servers/test_device_tagging.py
@@ -47,8 +47,8 @@
raise cls.skipException('Neutron is required')
if not CONF.validation.run_validation:
raise cls.skipException('Validation must be enabled')
- if (not CONF.compute_feature_enabled.config_drive
- and not CONF.compute_feature_enabled.metadata_service):
+ if (not CONF.compute_feature_enabled.config_drive and
+ not CONF.compute_feature_enabled.metadata_service):
raise cls.skipException('One of metadata or config drive must be '
'enabled')
@@ -134,7 +134,6 @@
self.addCleanup(self.ports_client.delete_port, self.port2['id'])
# Create server
- admin_pass = data_utils.rand_password()
config_drive_enabled = CONF.compute_feature_enabled.config_drive
validation_resources = self.get_test_validation_resources(
self.os_primary)
@@ -144,7 +143,6 @@
wait_until='ACTIVE',
validation_resources=validation_resources,
config_drive=config_drive_enabled,
- adminPass=admin_pass,
name=data_utils.rand_name('device-tagging-server'),
networks=[
# Validation network for ssh
@@ -212,8 +210,7 @@
self.ssh_client = remote_client.RemoteClient(
self.get_server_ip(server, validation_resources),
CONF.validation.image_ssh_user,
- admin_pass,
- validation_resources['keypair']['private_key'],
+ pkey=validation_resources['keypair']['private_key'],
server=server,
servers_client=self.servers_client)
diff --git a/tempest/api/compute/servers/test_list_servers_negative.py b/tempest/api/compute/servers/test_list_servers_negative.py
index 393e68f..18a78f0 100644
--- a/tempest/api/compute/servers/test_list_servers_negative.py
+++ b/tempest/api/compute/servers/test_list_servers_negative.py
@@ -79,10 +79,16 @@
@decorators.attr(type=['negative'])
@decorators.idempotent_id('fcdf192d-0f74-4d89-911f-1ec002b822c4')
def test_list_servers_status_non_existing(self):
- # Return an empty list when invalid status is specified
- body = self.client.list_servers(status='non_existing_status')
- servers = body['servers']
- self.assertEmpty(servers)
+ # When invalid status is specified, up to microversion 2.37,
+ # an empty list is returnd, and starting from microversion 2.38,
+ # a 400 error is returned in that case.
+ if self.is_requested_microversion_compatible('2.37'):
+ body = self.client.list_servers(status='non_existing_status')
+ servers = body['servers']
+ self.assertEmpty(servers)
+ else:
+ self.assertRaises(lib_exc.BadRequest, self.client.list_servers,
+ status='non_existing_status')
@decorators.attr(type=['negative'])
@decorators.idempotent_id('d47c17fb-eebd-4287-8e95-f20a7e627b18')
diff --git a/tempest/api/compute/servers/test_novnc.py b/tempest/api/compute/servers/test_novnc.py
index d9581e3..1dfd0f9 100644
--- a/tempest/api/compute/servers/test_novnc.py
+++ b/tempest/api/compute/servers/test_novnc.py
@@ -151,11 +151,22 @@
self.assertTrue(
self._websocket.response.startswith(b'HTTP/1.1 101 Switching '
b'Protocols\r\n'),
- 'Did not get the expected 101 on the websockify call: '
- + six.text_type(self._websocket.response))
- self.assertTrue(
- self._websocket.response.find(b'Server: WebSockify') > 0,
- 'Did not get the expected WebSocket HTTP Response.')
+ 'Did not get the expected 101 on the {} call: {}'.format(
+ CONF.compute_feature_enabled.vnc_server_header,
+ six.text_type(self._websocket.response)
+ )
+ )
+ # Since every other server type returns Headers with different case
+ # (for example 'nginx'), lowercase must be applied to eliminate issues.
+ _desired_header = "server: {0}".format(
+ CONF.compute_feature_enabled.vnc_server_header
+ ).lower()
+ _response = six.text_type(self._websocket.response).lower()
+ self.assertIn(
+ _desired_header,
+ _response,
+ 'Did not get the expected WebSocket HTTP Response.'
+ )
@decorators.idempotent_id('c640fdff-8ab4-45a4-a5d8-7e6146cbd0dc')
def test_novnc(self):
diff --git a/tempest/api/compute/servers/test_server_actions.py b/tempest/api/compute/servers/test_server_actions.py
index 5c3e9f0..bbec30c 100644
--- a/tempest/api/compute/servers/test_server_actions.py
+++ b/tempest/api/compute/servers/test_server_actions.py
@@ -586,7 +586,7 @@
server_info = self.client.show_server(self.server_id)['server']
if 'SHELVED' in server_info['status']:
self.client.unshelve_server(self.server_id)
- self.addOnException(_unshelve_server)
+ self.addCleanup(_unshelve_server)
server = self.client.show_server(self.server_id)['server']
image_name = server['name'] + '-shelved'
diff --git a/tempest/api/compute/servers/test_servers.py b/tempest/api/compute/servers/test_servers.py
index 2904976..543fa1c 100644
--- a/tempest/api/compute/servers/test_servers.py
+++ b/tempest/api/compute/servers/test_servers.py
@@ -184,8 +184,28 @@
min_microversion = '2.47'
max_microversion = 'latest'
+ # NOTE(gmann): This test tests the server APIs response schema
+ # Along with 2.47 microversion schema this test class tests the
+ # other microversions 2.9, 2.19 and 2.26 server APIs response schema
+ # also. 2.47 APIs schema are on top of 2.9->2.19->2.26 schema so
+ # below tests cover all of the schema.
+
@decorators.idempotent_id('88b0bdb2-494c-11e7-a919-92ebcb67fe33')
def test_show_server(self):
server = self.create_test_server()
# All fields will be checked by API schema
self.servers_client.show_server(server['id'])
+
+ @decorators.idempotent_id('8de397c2-57d0-4b90-aa30-e5d668f21a8b')
+ def test_update_rebuild_list_server(self):
+ server = self.create_test_server()
+ # Checking update API response schema
+ self.servers_client.update_server(server['id'])
+ waiters.wait_for_server_status(self.servers_client, server['id'],
+ 'ACTIVE')
+ # Checking rebuild API response schema
+ self.servers_client.rebuild_server(server['id'], self.image_ref_alt)
+ waiters.wait_for_server_status(self.servers_client,
+ server['id'], 'ACTIVE')
+ # Checking list details API response schema
+ self.servers_client.list_servers(detail=True)
diff --git a/tempest/api/compute/servers/test_servers_microversions.py b/tempest/api/compute/servers/test_servers_microversions.py
new file mode 100644
index 0000000..f3863f1
--- /dev/null
+++ b/tempest/api/compute/servers/test_servers_microversions.py
@@ -0,0 +1,51 @@
+# Copyright 2018 NEC Corporation.
+# 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.compute import base
+from tempest.common import waiters
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+
+# NOTE(gmann): This file is to write the tests which mainly
+# tests newly added microversion schema related to servers APIs.
+# As per (https://docs.openstack.org/tempest/latest/microversion_testing.
+# html#tempest-scope-for-microversion-testing),
+# we need to fill the API response schema gaps which gets modified
+# during microversion change. To cover the testing of such schema
+# we need to have operation schema test which just test
+# the microversion schemas.
+# If you are adding server APIs microversion schema file without
+# their integration tests, you can add tests to cover those schema
+# in this file.
+
+
+class ServerShowV254Test(base.BaseV2ComputeTest):
+ min_microversion = '2.54'
+ max_microversion = 'latest'
+
+ @decorators.idempotent_id('09170a98-4940-4637-add7-1a35121f1a5a')
+ def test_rebuild_server(self):
+ server = self.create_test_server(wait_until='ACTIVE')
+ keypair_name = data_utils.rand_name(
+ self.__class__.__name__ + '-keypair')
+ kwargs = {'name': keypair_name}
+ self.keypairs_client.create_keypair(**kwargs)
+ self.addCleanup(self.keypairs_client.delete_keypair,
+ keypair_name)
+ # Checking rebuild API response schema
+ self.servers_client.rebuild_server(server['id'], self.image_ref_alt,
+ key_name=keypair_name)
+ waiters.wait_for_server_status(self.servers_client,
+ server['id'], 'ACTIVE')
diff --git a/tempest/api/compute/servers/test_servers_negative.py b/tempest/api/compute/servers/test_servers_negative.py
index 9b545af..e944c28 100644
--- a/tempest/api/compute/servers/test_servers_negative.py
+++ b/tempest/api/compute/servers/test_servers_negative.py
@@ -481,7 +481,7 @@
server_info = self.client.show_server(self.server_id)['server']
if 'SHELVED' in server_info['status']:
self.client.unshelve_server(self.server_id)
- self.addOnException(_unshelve_server)
+ self.addCleanup(_unshelve_server)
server = self.client.show_server(self.server_id)['server']
image_name = server['name'] + '-shelved'
diff --git a/tempest/api/compute/test_tenant_networks.py b/tempest/api/compute/test_tenant_networks.py
index b55e2c0..f4eada0 100644
--- a/tempest/api/compute/test_tenant_networks.py
+++ b/tempest/api/compute/test_tenant_networks.py
@@ -18,6 +18,7 @@
class ComputeTenantNetworksTest(base.BaseV2ComputeTest):
+ max_microversion = '2.35'
@classmethod
def resource_setup(cls):
diff --git a/tempest/api/compute/volumes/test_attach_volume_negative.py b/tempest/api/compute/volumes/test_attach_volume_negative.py
index 7a74869..8618148 100644
--- a/tempest/api/compute/volumes/test_attach_volume_negative.py
+++ b/tempest/api/compute/volumes/test_attach_volume_negative.py
@@ -56,3 +56,16 @@
self.assertRaises(lib_exc.BadRequest,
self.attach_volume, server, volume)
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('ee37a796-2afb-11e7-bc0f-fa163e65f5ce')
+ def test_attach_attached_volume_to_different_server(self):
+ server1 = self.create_test_server(wait_until='ACTIVE')
+ volume = self.create_volume()
+
+ self.attach_volume(server1, volume)
+
+ # Create server2 and attach in-use volume
+ server2 = self.create_test_server(wait_until='ACTIVE')
+ self.assertRaises(lib_exc.BadRequest,
+ self.attach_volume, server2, volume)
diff --git a/tempest/api/identity/admin/v2/test_tenants.py b/tempest/api/identity/admin/v2/test_tenants.py
index 0f955bf..cda721c 100644
--- a/tempest/api/identity/admin/v2/test_tenants.py
+++ b/tempest/api/identity/admin/v2/test_tenants.py
@@ -59,11 +59,9 @@
# Create a tenant that is enabled
tenant = self.setup_test_tenant(enabled=True)
tenant_id = tenant['id']
- en1 = tenant['enabled']
- self.assertTrue(en1, 'Enable should be True in response')
+ self.assertTrue(tenant['enabled'], 'Enable should be True in response')
body = self.tenants_client.show_tenant(tenant_id)['tenant']
- en2 = body['enabled']
- self.assertTrue(en2, 'Enable should be True in lookup')
+ self.assertTrue(body['enabled'], 'Enable should be True in lookup')
self.tenants_client.delete_tenant(tenant_id)
@decorators.idempotent_id('3be22093-b30f-499d-b772-38340e5e16fb')
@@ -71,12 +69,10 @@
# Create a tenant that is not enabled
tenant = self.setup_test_tenant(enabled=False)
tenant_id = tenant['id']
- en1 = tenant['enabled']
- self.assertEqual('false', str(en1).lower(),
+ self.assertFalse(tenant['enabled'],
'Enable should be False in response')
body = self.tenants_client.show_tenant(tenant_id)['tenant']
- en2 = body['enabled']
- self.assertEqual('false', str(en2).lower(),
+ self.assertFalse(body['enabled'],
'Enable should be False in lookup')
self.tenants_client.delete_tenant(tenant_id)
@@ -143,7 +139,7 @@
resp3_en = body['enabled']
self.assertNotEqual(resp1_en, resp3_en)
- self.assertEqual('false', str(resp1_en).lower())
+ self.assertFalse(tenant['enabled'])
self.assertEqual(resp2_en, resp3_en)
self.tenants_client.delete_tenant(t_id)
diff --git a/tempest/api/identity/admin/v3/test_application_credentials.py b/tempest/api/identity/admin/v3/test_application_credentials.py
new file mode 100644
index 0000000..4a74ef8
--- /dev/null
+++ b/tempest/api/identity/admin/v3/test_application_credentials.py
@@ -0,0 +1,48 @@
+# Copyright 2018 SUSE Linux GmbH
+#
+# 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.identity import base
+from tempest import config
+from tempest.lib import decorators
+
+
+CONF = config.CONF
+
+
+class ApplicationCredentialsV3AdminTest(base.BaseApplicationCredentialsV3Test,
+ base.BaseIdentityV3AdminTest):
+
+ @decorators.idempotent_id('3b3dd48f-3388-406a-a9e6-4d078a552d0e')
+ def test_create_application_credential_with_roles(self):
+ role = self.setup_test_role()
+ self.os_admin.roles_v3_client.create_user_role_on_project(
+ self.project_id,
+ self.user_id,
+ role['id']
+ )
+
+ app_cred = self.create_application_credential(
+ roles=[{'id': role['id']}])
+ secret = app_cred['secret']
+
+ # Check that the application credential is functional
+ token_id, resp = self.non_admin_token.get_token(
+ app_cred_id=app_cred['id'],
+ app_cred_secret=secret,
+ auth_data=True
+ )
+ self.assertEqual(resp['project']['id'], self.project_id)
+ self.assertEqual(resp['roles'][0]['id'], role['id'])
diff --git a/tempest/api/identity/admin/v3/test_project_tags.py b/tempest/api/identity/admin/v3/test_project_tags.py
new file mode 100644
index 0000000..d05173b
--- /dev/null
+++ b/tempest/api/identity/admin/v3/test_project_tags.py
@@ -0,0 +1,66 @@
+# Copyright 2018 AT&T Corporation.
+# 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 testtools
+
+from tempest.api.identity import base
+from tempest import config
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
+
+CONF = config.CONF
+
+
+class IdentityV3ProjectTagsTest(base.BaseIdentityV3AdminTest):
+
+ @decorators.idempotent_id('7c123aac-999d-416a-a0fb-84b915ab10de')
+ @testtools.skipUnless(CONF.identity_feature_enabled.project_tags,
+ 'Project tags not available.')
+ def test_list_update_delete_project_tags(self):
+ project = self.setup_test_project()
+
+ # Create a tag for testing.
+ tag = data_utils.rand_name('tag')
+ # NOTE(felipemonteiro): The response body for create is empty.
+ self.project_tags_client.update_project_tag(project['id'], tag)
+
+ # Verify that the tag was created.
+ self.project_tags_client.check_project_tag_existence(
+ project['id'], tag)
+
+ # Verify that updating the project tags works.
+ tags_to_update = [data_utils.rand_name('tag') for _ in range(3)]
+ updated_tags = self.project_tags_client.update_all_project_tags(
+ project['id'], tags_to_update)['tags']
+ self.assertEqual(sorted(tags_to_update), sorted(updated_tags))
+
+ # Verify that listing project tags works.
+ retrieved_tags = self.project_tags_client.list_project_tags(
+ project['id'])['tags']
+ self.assertEqual(sorted(tags_to_update), sorted(retrieved_tags))
+
+ # Verify that deleting a project tag works.
+ self.project_tags_client.delete_project_tag(
+ project['id'], tags_to_update[0])
+ self.assertRaises(lib_exc.NotFound,
+ self.project_tags_client.check_project_tag_existence,
+ project['id'], tags_to_update[0])
+
+ # Verify that deleting all project tags works.
+ self.project_tags_client.delete_all_project_tags(project['id'])
+ retrieved_tags = self.project_tags_client.list_project_tags(
+ project['id'])['tags']
+ self.assertEmpty(retrieved_tags)
diff --git a/tempest/api/identity/admin/v3/test_projects.py b/tempest/api/identity/admin/v3/test_projects.py
index ac23067..bc94a8e 100644
--- a/tempest/api/identity/admin/v3/test_projects.py
+++ b/tempest/api/identity/admin/v3/test_projects.py
@@ -101,22 +101,19 @@
# Create a project that is enabled
project = self.setup_test_project(enabled=True)
project_id = project['id']
- en1 = project['enabled']
- self.assertTrue(en1, 'Enable should be True in response')
+ self.assertTrue(project['enabled'],
+ 'Enable should be True in response')
body = self.projects_client.show_project(project_id)['project']
- en2 = body['enabled']
- self.assertTrue(en2, 'Enable should be True in lookup')
+ self.assertTrue(body['enabled'], 'Enable should be True in lookup')
@decorators.idempotent_id('78f96a9c-e0e0-4ee6-a3ba-fbf6dfd03207')
def test_project_create_not_enabled(self):
# Create a project that is not enabled
project = self.setup_test_project(enabled=False)
- en1 = project['enabled']
- self.assertEqual('false', str(en1).lower(),
+ self.assertFalse(project['enabled'],
'Enable should be False in response')
body = self.projects_client.show_project(project['id'])['project']
- en2 = body['enabled']
- self.assertEqual('false', str(en2).lower(),
+ self.assertFalse(body['enabled'],
'Enable should be False in lookup')
@decorators.idempotent_id('f608f368-048c-496b-ad63-d286c26dab6b')
@@ -178,7 +175,7 @@
resp3_en = body['enabled']
self.assertNotEqual(resp1_en, resp3_en)
- self.assertEqual('false', str(resp1_en).lower())
+ self.assertFalse(project['enabled'])
self.assertEqual(resp2_en, resp3_en)
@decorators.idempotent_id('59398d4a-5dc5-4f86-9a4c-c26cc804d6c6')
diff --git a/tempest/api/identity/base.py b/tempest/api/identity/base.py
index 9edccbb..68f2c07 100644
--- a/tempest/api/identity/base.py
+++ b/tempest/api/identity/base.py
@@ -190,6 +190,8 @@
cls.non_admin_catalog_client = cls.os_primary.catalog_client
cls.non_admin_versions_client =\
cls.os_primary.identity_versions_v3_client
+ cls.non_admin_app_creds_client = \
+ cls.os_primary.application_credentials_client
class BaseIdentityV3AdminTest(BaseIdentityV3Test):
@@ -228,6 +230,7 @@
cls.domain_config_client = cls.os_admin.domain_config_client
cls.endpoint_filter_client = cls.os_admin.endpoint_filter_client
cls.endpoint_groups_client = cls.os_admin.endpoint_groups_client
+ cls.project_tags_client = cls.os_admin.project_tags_client
if CONF.identity.admin_domain_scope:
# NOTE(andreaf) When keystone policy requires it, the identity
@@ -288,3 +291,30 @@
test_utils.call_and_ignore_notfound_exc,
self.delete_domain, domain['id'])
return domain
+
+
+class BaseApplicationCredentialsV3Test(BaseIdentityV3Test):
+
+ @classmethod
+ def skip_checks(cls):
+ super(BaseApplicationCredentialsV3Test, cls).skip_checks()
+ if not CONF.identity_feature_enabled.application_credentials:
+ raise cls.skipException("Application credentials are not available"
+ " in this environment")
+
+ @classmethod
+ def resource_setup(cls):
+ super(BaseApplicationCredentialsV3Test, cls).resource_setup()
+ cls.user_id = cls.os_primary.credentials.user_id
+ cls.project_id = cls.os_primary.credentials.project_id
+
+ def create_application_credential(self, name=None, **kwargs):
+ name = name or data_utils.rand_name('application_credential')
+ application_credential = (
+ self.non_admin_app_creds_client.create_application_credential(
+ self.user_id, name=name, **kwargs))['application_credential']
+ self.addCleanup(
+ self.non_admin_app_creds_client.delete_application_credential,
+ self.user_id,
+ application_credential['id'])
+ return application_credential
diff --git a/tempest/api/identity/v3/test_application_credentials.py b/tempest/api/identity/v3/test_application_credentials.py
new file mode 100644
index 0000000..caf0b1e
--- /dev/null
+++ b/tempest/api/identity/v3/test_application_credentials.py
@@ -0,0 +1,85 @@
+# Copyright 2018 SUSE Linux GmbH
+#
+# 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 datetime
+
+from oslo_utils import timeutils
+
+from tempest.api.identity import base
+from tempest import config
+from tempest.lib import decorators
+
+
+CONF = config.CONF
+
+
+class ApplicationCredentialsV3Test(base.BaseApplicationCredentialsV3Test):
+
+ def _list_app_creds(self, name=None):
+ kwargs = dict(user_id=self.user_id)
+ if name:
+ kwargs.update(name=name)
+ return self.non_admin_app_creds_client.list_application_credentials(
+ **kwargs)['application_credentials']
+
+ @decorators.idempotent_id('8080c75c-eddc-4786-941a-c2da7039ae61')
+ def test_create_application_credential(self):
+ app_cred = self.create_application_credential()
+
+ # Check that the secret appears in the create response
+ secret = app_cred['secret']
+
+ # Check that the secret is not retrievable after initial create
+ app_cred = self.non_admin_app_creds_client.show_application_credential(
+ user_id=self.user_id,
+ application_credential_id=app_cred['id']
+ )['application_credential']
+ self.assertNotIn('secret', app_cred)
+
+ # Check that the application credential is functional
+ token_id, resp = self.non_admin_token.get_token(
+ app_cred_id=app_cred['id'],
+ app_cred_secret=secret,
+ auth_data=True
+ )
+ self.assertEqual(resp['project']['id'], self.project_id)
+
+ @decorators.idempotent_id('852daf0c-42b5-4239-8466-d193d0543ed3')
+ def test_create_application_credential_expires(self):
+ expires_at = timeutils.utcnow() + datetime.timedelta(hours=1)
+
+ app_cred = self.create_application_credential(expires_at=expires_at)
+
+ expires_str = expires_at.isoformat()
+ self.assertEqual(expires_str, app_cred['expires_at'])
+
+ @decorators.idempotent_id('ff0cd457-6224-46e7-b79e-0ada4964a8a6')
+ def test_list_application_credentials(self):
+ self.create_application_credential()
+ self.create_application_credential()
+
+ app_creds = self._list_app_creds()
+ self.assertEqual(2, len(app_creds))
+
+ @decorators.idempotent_id('9bb5e5cc-5250-493a-8869-8b665f6aa5f6')
+ def test_query_application_credentials(self):
+ self.create_application_credential()
+ app_cred_two = self.create_application_credential()
+ app_cred_two_name = app_cred_two['name']
+
+ app_creds = self._list_app_creds(name=app_cred_two_name)
+ self.assertEqual(1, len(app_creds))
+ self.assertEqual(app_cred_two_name, app_creds[0]['name'])
diff --git a/tempest/api/image/v1/test_images.py b/tempest/api/image/v1/test_images.py
index 76723f4..2432c8b 100644
--- a/tempest/api/image/v1/test_images.py
+++ b/tempest/api/image/v1/test_images.py
@@ -132,8 +132,8 @@
@classmethod
def skip_checks(cls):
super(ListImagesTest, cls).skip_checks()
- if (len(CONF.image.container_formats) < 2
- or len(CONF.image.disk_formats) < 2):
+ if (len(CONF.image.container_formats) < 2 or
+ len(CONF.image.disk_formats) < 2):
skip_msg = ("%s skipped as multiple container formats "
"or disk formats are not available." % cls.__name__)
raise cls.skipException(skip_msg)
@@ -227,8 +227,8 @@
self.assertEqual(image['disk_format'], self.disk_format_alt)
result_set = set(map(lambda x: x['id'], images_list))
self.assertTrue(self.same_disk_format_set <= result_set)
- self.assertFalse(self.created_set - self.same_disk_format_set
- <= result_set)
+ self.assertFalse(self.created_set - self.same_disk_format_set <=
+ result_set)
@decorators.idempotent_id('2143655d-96d9-4bec-9188-8674206b4b3b')
def test_index_container_format(self):
@@ -238,8 +238,8 @@
self.assertEqual(image['container_format'], self.container_format)
result_set = set(map(lambda x: x['id'], images_list))
self.assertTrue(self.same_container_format_set <= result_set)
- self.assertFalse(self.created_set - self.same_container_format_set
- <= result_set)
+ self.assertFalse(self.created_set - self.same_container_format_set <=
+ result_set)
@decorators.idempotent_id('feb32ac6-22bb-4a16-afd8-9454bb714b14')
def test_index_max_size(self):
diff --git a/tempest/api/image/v2/test_images.py b/tempest/api/image/v2/test_images.py
index ce5bd3e..aa57daf 100644
--- a/tempest/api/image/v2/test_images.py
+++ b/tempest/api/image/v2/test_images.py
@@ -71,8 +71,12 @@
self.assertEqual(1024, body.get('size'))
# Now try get image file
+ # NOTE: This Glance API returns different status codes for image
+ # condition. In this non-empty data case, Glance should return 200,
+ # so here should check the status code.
body = self.client.show_image_file(image['id'])
self.assertEqual(file_content, body.data)
+ self.assertEqual(200, body.response.status)
@decorators.attr(type='smoke')
@decorators.idempotent_id('f848bb94-1c6e-45a4-8726-39e3a5b23535')
@@ -111,6 +115,13 @@
visibility='private')
self.assertEqual('queued', image['status'])
+ # NOTE: This Glance API returns different status codes for image
+ # condition. In this empty data case, Glance should return 204,
+ # so here should check the status code.
+ image_file = self.client.show_image_file(image['id'])
+ self.assertEqual(0, len(image_file.data))
+ self.assertEqual(204, image_file.response.status)
+
# Now try uploading an image file
image_file = six.BytesIO(data_utils.random_bytes())
self.client.store_image_file(image['id'], image_file)
diff --git a/tempest/api/volume/admin/test_volume_hosts.py b/tempest/api/volume/admin/test_volume_hosts.py
index ce0cbd2..7e53ce8 100644
--- a/tempest/api/volume/admin/test_volume_hosts.py
+++ b/tempest/api/volume/admin/test_volume_hosts.py
@@ -46,8 +46,8 @@
# show host API should fail (return code: 404). The cinder-volume host
# is presented in format: <host-name>@driver-name.
c_vol_hosts = [host['host_name'] for host in hosts
- if (host['service'] == 'cinder-volume'
- and host['service-state'] == 'enabled')]
+ if (host['service'] == 'cinder-volume' and
+ host['service-state'] == 'enabled')]
self.assertNotEmpty(c_vol_hosts,
"No available cinder-volume host is found, "
"all hosts that found are: %s" % hosts)
diff --git a/tempest/api/volume/admin/test_volume_retype_with_migration.py b/tempest/api/volume/admin/test_volume_retype_with_migration.py
index f0b3a4f..025c1be 100644
--- a/tempest/api/volume/admin/test_volume_retype_with_migration.py
+++ b/tempest/api/volume/admin/test_volume_retype_with_migration.py
@@ -46,13 +46,10 @@
extra_specs_src = {"volume_backend_name": backend_src}
extra_specs_dst = {"volume_backend_name": backend_dst}
- src_vol_type = cls.create_volume_type(extra_specs=extra_specs_src)
+ cls.src_vol_type = cls.create_volume_type(extra_specs=extra_specs_src)
cls.dst_vol_type = cls.create_volume_type(extra_specs=extra_specs_dst)
- cls.src_vol = cls.create_volume(volume_type=src_vol_type['name'])
-
- @classmethod
- def resource_cleanup(cls):
+ def _wait_for_internal_volume_cleanup(self, vol):
# When retyping a volume, Cinder creates an internal volume in the
# target backend. The volume in the source backend is deleted after
# the migration, so we need to wait for Cinder delete this volume
@@ -60,40 +57,37 @@
# This list should return 2 volumes until the copy and cleanup
# process is finished.
- fetched_list = cls.admin_volume_client.list_volumes(
+ fetched_list = self.admin_volume_client.list_volumes(
params={'all_tenants': True,
- 'display_name': cls.src_vol['name']})['volumes']
+ 'display_name': vol['name']})['volumes']
for fetched_vol in fetched_list:
- if fetched_vol['id'] != cls.src_vol['id']:
+ if fetched_vol['id'] != vol['id']:
# This is the Cinder internal volume
LOG.debug('Waiting for internal volume %s deletion',
fetched_vol['id'])
- cls.admin_volume_client.wait_for_resource_deletion(
+ self.admin_volume_client.wait_for_resource_deletion(
fetched_vol['id'])
break
- super(VolumeRetypeWithMigrationTest, cls).resource_cleanup()
-
- @decorators.idempotent_id('a1a41f3f-9dad-493e-9f09-3ff197d477cd')
- def test_available_volume_retype_with_migration(self):
-
+ def _retype_volume(self, volume):
keys_with_no_change = ('id', 'size', 'description', 'name', 'user_id',
'os-vol-tenant-attr:tenant_id')
keys_with_change = ('volume_type', 'os-vol-host-attr:host')
volume_source = self.admin_volume_client.show_volume(
- self.src_vol['id'])['volume']
+ volume['id'])['volume']
self.volumes_client.retype_volume(
- self.src_vol['id'],
+ volume['id'],
new_type=self.dst_vol_type['name'],
migration_policy='on-demand')
-
- waiters.wait_for_volume_retype(self.volumes_client, self.src_vol['id'],
+ self.addCleanup(self._wait_for_internal_volume_cleanup, volume)
+ waiters.wait_for_volume_retype(self.volumes_client, volume['id'],
self.dst_vol_type['name'])
+
volume_dest = self.admin_volume_client.show_volume(
- self.src_vol['id'])['volume']
+ volume['id'])['volume']
# Check the volume information after the migration.
self.assertEqual('success',
@@ -105,3 +99,27 @@
for key in keys_with_change:
self.assertNotEqual(volume_source[key], volume_dest[key])
+
+ @decorators.idempotent_id('a1a41f3f-9dad-493e-9f09-3ff197d477cd')
+ def test_available_volume_retype_with_migration(self):
+ src_vol = self.create_volume(volume_type=self.src_vol_type['name'])
+ self._retype_volume(src_vol)
+
+ @decorators.idempotent_id('d0d9554f-e7a5-4104-8973-f35b27ccb60d')
+ def test_volume_from_snapshot_retype_with_migration(self):
+ # Create a volume in the first backend
+ src_vol = self.create_volume(volume_type=self.src_vol_type['name'])
+
+ # Create a volume snapshot
+ snapshot = self.create_snapshot(src_vol['id'])
+
+ # Create a volume from the snapshot
+ src_vol = self.create_volume(volume_type=self.src_vol_type['name'],
+ snapshot_id=snapshot['id'])
+
+ # Delete the snapshot
+ self.snapshots_client.delete_snapshot(snapshot['id'])
+ self.snapshots_client.wait_for_resource_deletion(snapshot['id'])
+
+ # Migrate the volume from snapshot to the second backend
+ self._retype_volume(src_vol)
diff --git a/tempest/api/volume/test_volumes_backup.py b/tempest/api/volume/test_volumes_backup.py
index 552b231..07cfad5 100644
--- a/tempest/api/volume/test_volumes_backup.py
+++ b/tempest/api/volume/test_volumes_backup.py
@@ -128,7 +128,7 @@
volume_details = self.volumes_client.show_volume(
volume['id'])['volume']
- self.assertEqual('true', volume_details['bootable'])
+ self.assertTrue(volume_details['bootable'])
# Create a backup
backup = self.create_backup(volume_id=volume['id'])
@@ -140,7 +140,7 @@
restored_volume_info = self.volumes_client.show_volume(
restored_volume_id)['volume']
- self.assertEqual('true', restored_volume_info['bootable'])
+ self.assertTrue(restored_volume_info['bootable'])
class VolumesBackupsV39Test(base.BaseVolumeTest):
diff --git a/tempest/api/volume/test_volumes_extend.py b/tempest/api/volume/test_volumes_extend.py
index 54052ae..5d339c4 100644
--- a/tempest/api/volume/test_volumes_extend.py
+++ b/tempest/api/volume/test_volumes_extend.py
@@ -32,7 +32,7 @@
@decorators.idempotent_id('9a36df71-a257-43a5-9555-dc7c88e66e0e')
def test_volume_extend(self):
# Extend Volume Test.
- volume = self.create_volume()
+ volume = self.create_volume(image_ref=self.image_ref)
extend_size = volume['size'] + 1
self.volumes_client.extend_volume(volume['id'],
new_size=extend_size)
@@ -44,7 +44,6 @@
@decorators.idempotent_id('86be1cba-2640-11e5-9c82-635fb964c912')
@testtools.skipUnless(CONF.volume_feature_enabled.snapshot,
"Cinder volume snapshots are disabled")
- @decorators.skip_because(bug='1687044')
def test_volume_extend_when_volume_has_snapshot(self):
volume = self.create_volume()
self.create_snapshot(volume['id'])
diff --git a/tempest/api/volume/test_volumes_snapshots.py b/tempest/api/volume/test_volumes_snapshots.py
index dcd3518..52114bc 100644
--- a/tempest/api/volume/test_volumes_snapshots.py
+++ b/tempest/api/volume/test_volumes_snapshots.py
@@ -41,16 +41,19 @@
def test_snapshot_create_delete_with_volume_in_use(self):
# Create a test instance
server = self.create_server()
- self.attach_volume(server['id'], self.volume_origin['id'])
+ # NOTE(zhufl) Here we create volume from self.image_ref for adding
+ # coverage for "creating snapshot from non-blank volume".
+ volume = self.create_volume(image_ref=self.image_ref)
+ self.attach_volume(server['id'], volume['id'])
# Snapshot a volume which attached to an instance with force=False
self.assertRaises(lib_exc.BadRequest, self.create_snapshot,
- self.volume_origin['id'], force=False)
+ volume['id'], force=False)
# Snapshot a volume attached to an instance
- snapshot1 = self.create_snapshot(self.volume_origin['id'], force=True)
- snapshot2 = self.create_snapshot(self.volume_origin['id'], force=True)
- snapshot3 = self.create_snapshot(self.volume_origin['id'], force=True)
+ snapshot1 = self.create_snapshot(volume['id'], force=True)
+ snapshot2 = self.create_snapshot(volume['id'], force=True)
+ snapshot3 = self.create_snapshot(volume['id'], force=True)
# Delete the snapshots. Some snapshot implementations can take
# different paths according to order they are deleted.
diff --git a/tempest/api/volume/test_volumes_snapshots_list.py b/tempest/api/volume/test_volumes_snapshots_list.py
index 507df1f..f12bfd8 100644
--- a/tempest/api/volume/test_volumes_snapshots_list.py
+++ b/tempest/api/volume/test_volumes_snapshots_list.py
@@ -28,13 +28,11 @@
@classmethod
def resource_setup(cls):
super(VolumesSnapshotListTestJSON, cls).resource_setup()
- cls.snapshot_id_list = []
volume_origin = cls.create_volume()
# Create snapshots with params
for _ in range(3):
snapshot = cls.create_snapshot(volume_origin['id'])
- cls.snapshot_id_list.append(snapshot['id'])
cls.snapshot = snapshot
def _list_by_param_values_and_assert(self, with_detail=False, **params):
@@ -151,10 +149,14 @@
@decorators.idempotent_id('05489dde-44bc-4961-a1f5-3ce7ee7824f7')
def test_snapshot_list_param_marker(self):
# The list of snapshots should end before the provided marker
- params = {'marker': self.snapshot_id_list[1]}
+ snap_list = self.snapshots_client.list_snapshots()['snapshots']
+ # list_snapshots will take the reverse order as they are created.
+ snapshot_id_list = [snap['id'] for snap in snap_list][::-1]
+
+ params = {'marker': snapshot_id_list[1]}
snap_list = self.snapshots_client.list_snapshots(**params)['snapshots']
fetched_list_id = [snap['id'] for snap in snap_list]
# Verify the list of snapshots ends before the provided
# marker(second snapshot), therefore only the first snapshot
# should displayed.
- self.assertEqual(self.snapshot_id_list[:1], fetched_list_id)
+ self.assertEqual(snapshot_id_list[:1], fetched_list_id)
diff --git a/tempest/clients.py b/tempest/clients.py
index b06eafb..707127c 100644
--- a/tempest/clients.py
+++ b/tempest/clients.py
@@ -197,6 +197,10 @@
self.endpoint_groups_client = self.identity_v3.EndPointGroupsClient(
**params_v3)
self.catalog_client = self.identity_v3.CatalogClient(**params_v3)
+ self.project_tags_client = self.identity_v3.ProjectTagsClient(
+ **params_v3)
+ self.application_credentials_client = \
+ self.identity_v3.ApplicationCredentialsClient(**params_v3)
# Token clients do not use the catalog. They only need default_params.
# They read auth_url, so they should only be set if the corresponding
@@ -267,6 +271,7 @@
# Set default client for users that don't need explicit version
self.volumes_client_latest = self.volumes_v2_client
self.snapshots_client_latest = self.snapshots_v2_client
+ self.backups_client_latest = self.backups_v2_client
if CONF.volume_feature_enabled.api_v3:
self.backups_v3_client = self.volume_v3.BackupsClient()
@@ -282,6 +287,7 @@
# Set default client for users that don't need explicit version
self.volumes_client_latest = self.volumes_v3_client
self.snapshots_client_latest = self.snapshots_v3_client
+ self.backups_client_latest = self.backups_v3_client
def _set_object_storage_clients(self):
self.account_client = self.object_storage.AccountClient()
diff --git a/tempest/cmd/cleanup_service.py b/tempest/cmd/cleanup_service.py
index 025959a..27e1bc1 100644
--- a/tempest/cmd/cleanup_service.py
+++ b/tempest/cmd/cleanup_service.py
@@ -104,11 +104,11 @@
self.tenant_filter['tenant_id'] = self.tenant_id
def _filter_by_tenant_id(self, item_list):
- if (item_list is None
- or not item_list
- or not hasattr(self, 'tenant_id')
- or self.tenant_id is None
- or 'tenant_id' not in item_list[0]):
+ if (item_list is None or
+ not item_list or
+ not hasattr(self, 'tenant_id') or
+ self.tenant_id is None or
+ 'tenant_id' not in item_list[0]):
return item_list
return [item for item in item_list
@@ -816,8 +816,8 @@
if not self.is_save_state:
roles = [role for role in roles if
(role['id'] not in
- self.saved_state_json['roles'].keys()
- and role['name'] != CONF.identity.admin_role)]
+ self.saved_state_json['roles'].keys() and
+ role['name'] != CONF.identity.admin_role)]
LOG.debug("List count, %s Roles after reconcile", len(roles))
return roles
except Exception:
@@ -852,13 +852,16 @@
def list(self):
projects = self.client.list_projects()['projects']
if not self.is_save_state:
- projects = [project for project in projects if (project['id']
- not in self.saved_state_json['projects'].keys()
- and project['name'] != CONF.auth.admin_project_name)]
+ project_ids = self.saved_state_json['projects']
+ projects = [project
+ for project in projects
+ if (project['id'] not in project_ids and
+ project['name'] != CONF.auth.admin_project_name)]
if self.is_preserve:
- projects = [project for project in projects if project['name']
- not in CONF_PROJECTS]
+ projects = [project
+ for project in projects
+ if project['name'] not in CONF_PROJECTS]
LOG.debug("List count, %s Projects after reconcile", len(projects))
return projects
diff --git a/tempest/cmd/init.py b/tempest/cmd/init.py
index 9a85d89..84c8631 100644
--- a/tempest/cmd/init.py
+++ b/tempest/cmd/init.py
@@ -136,7 +136,7 @@
if not os.path.isdir(local_dir):
LOG.debug('Creating local working dir: %s', local_dir)
os.mkdir(local_dir)
- elif not os.listdir(local_dir) == []:
+ elif os.listdir(local_dir):
raise OSError("Directory you are trying to initialize already "
"exists and is not empty: %s" % local_dir)
diff --git a/tempest/common/waiters.py b/tempest/common/waiters.py
index 08e2a12..0e86f05 100644
--- a/tempest/common/waiters.py
+++ b/tempest/common/waiters.py
@@ -287,3 +287,24 @@
raise lib_exc.TimeoutException(message)
return body
+
+
+def wait_for_interface_detach(client, server_id, port_id):
+ """Waits for an interface to be detached from a server."""
+ body = client.list_interfaces(server_id)['interfaceAttachments']
+ ports = [iface['port_id'] for iface in body]
+ start = int(time.time())
+
+ while port_id in ports:
+ time.sleep(client.build_interval)
+ body = client.list_interfaces(server_id)['interfaceAttachments']
+ ports = [iface['port_id'] for iface in body]
+ if port_id not in ports:
+ return body
+
+ timed_out = int(time.time()) - start >= client.build_timeout
+ if timed_out:
+ message = ('Interface %s failed to detach from server %s within '
+ 'the required time (%s s)' % (port_id, server_id,
+ client.build_timeout))
+ raise lib_exc.TimeoutException(message)
diff --git a/tempest/config.py b/tempest/config.py
index 5d27efd..1fb5c8e 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -240,7 +240,13 @@
'settings enabled?'),
cfg.BoolOpt('project_tags',
default=False,
- help='Is the project tags identity v3 API available?')
+ help='Is the project tags identity v3 API available?'),
+ # Application credentials is a default feature in Queens. This config
+ # option can removed once Pike is EOL.
+ cfg.BoolOpt('application_credentials',
+ default=False,
+ help='Does the environment have application credentials '
+ 'enabled?')
]
compute_group = cfg.OptGroup(name='compute',
@@ -407,6 +413,10 @@
default=False,
help='Enable VNC console. This configuration value should '
'be same as [nova.vnc]->vnc_enabled in nova.conf'),
+ cfg.StrOpt('vnc_server_header',
+ default='WebSockify',
+ help='Expected VNC server name (WebSockify, nginx, etc) '
+ 'in response header.'),
cfg.BoolOpt('spice_console',
default=False,
help='Enable Spice console. This configuration value should '
@@ -742,7 +752,7 @@
help='Timeout in seconds to wait for a volume to become '
'available.'),
cfg.StrOpt('catalog_type',
- default='volume',
+ default='volumev3',
help="Catalog type of the Volume Service"),
cfg.StrOpt('region',
default='',
@@ -1068,7 +1078,7 @@
return opt_list
-# this should never be called outside of this class
+# This should never be called outside of this module
class TempestConfigPrivate(object):
"""Provides OpenStack configuration information."""
diff --git a/tempest/hacking/checks.py b/tempest/hacking/checks.py
index aae685c..b6e7f8c 100644
--- a/tempest/hacking/checks.py
+++ b/tempest/hacking/checks.py
@@ -228,12 +228,12 @@
if 'tempest/lib/' not in filename:
return
- if not ('from tempest' in logical_line
- or 'import tempest' in logical_line):
+ if not ('from tempest' in logical_line or
+ 'import tempest' in logical_line):
return
- if ('from tempest.lib' in logical_line
- or 'import tempest.lib' in logical_line):
+ if ('from tempest.lib' in logical_line or
+ 'import tempest.lib' in logical_line):
return
msg = ("T112: tempest.lib should not import local tempest code to avoid "
@@ -266,9 +266,9 @@
if 'tempest/lib/' not in filename:
return
- if ('tempest.config' in logical_line
- or 'from tempest import config' in logical_line
- or 'oslo_config' in logical_line):
+ if ('tempest.config' in logical_line or
+ 'from tempest import config' in logical_line or
+ 'oslo_config' in logical_line):
msg = ('T114: tempest.lib can not have any dependency on tempest '
'config.')
yield(0, msg)
diff --git a/tempest/hacking/ignored_list_T110.txt b/tempest/hacking/ignored_list_T110.txt
deleted file mode 100644
index 0e7e894..0000000
--- a/tempest/hacking/ignored_list_T110.txt
+++ /dev/null
@@ -1 +0,0 @@
-./tempest/services/object_storage/object_client.py
diff --git a/tempest/lib/api_schema/response/compute/v2_1/flavors.py b/tempest/lib/api_schema/response/compute/v2_1/flavors.py
index 547d94d..af5e67f 100644
--- a/tempest/lib/api_schema/response/compute/v2_1/flavors.py
+++ b/tempest/lib/api_schema/response/compute/v2_1/flavors.py
@@ -86,7 +86,7 @@
'status_code': [200]
}
-create_get_flavor_details = {
+create_update_get_flavor_details = {
'status_code': [200],
'response_body': {
'type': 'object',
diff --git a/tempest/lib/api_schema/response/compute/v2_19/servers.py b/tempest/lib/api_schema/response/compute/v2_19/servers.py
index 05cc32c..fd9e933 100644
--- a/tempest/lib/api_schema/response/compute/v2_19/servers.py
+++ b/tempest/lib/api_schema/response/compute/v2_19/servers.py
@@ -14,9 +14,9 @@
import copy
-from tempest.lib.api_schema.response.compute.v2_1 import servers as serversv21
from tempest.lib.api_schema.response.compute.v2_16 import servers \
as serversv216
+from tempest.lib.api_schema.response.compute.v2_9 import servers as serversv29
list_servers = copy.deepcopy(serversv216.list_servers)
@@ -32,20 +32,20 @@
list_servers_detail['response_body']['properties']['servers']['items'][
'required'].append('description')
-update_server = copy.deepcopy(serversv21.update_server)
+update_server = copy.deepcopy(serversv29.update_server)
update_server['response_body']['properties']['server'][
'properties'].update({'description': {'type': ['string', 'null']}})
update_server['response_body']['properties']['server'][
'required'].append('description')
-rebuild_server = copy.deepcopy(serversv21.rebuild_server)
+rebuild_server = copy.deepcopy(serversv29.rebuild_server)
rebuild_server['response_body']['properties']['server'][
'properties'].update({'description': {'type': ['string', 'null']}})
rebuild_server['response_body']['properties']['server'][
'required'].append('description')
rebuild_server_with_admin_pass = copy.deepcopy(
- serversv21.rebuild_server_with_admin_pass)
+ serversv29.rebuild_server_with_admin_pass)
rebuild_server_with_admin_pass['response_body']['properties']['server'][
'properties'].update({'description': {'type': ['string', 'null']}})
rebuild_server_with_admin_pass['response_body']['properties']['server'][
diff --git a/tempest/lib/api_schema/response/compute/v2_26/servers.py b/tempest/lib/api_schema/response/compute/v2_26/servers.py
index b03bdf6..5c35eab 100644
--- a/tempest/lib/api_schema/response/compute/v2_26/servers.py
+++ b/tempest/lib/api_schema/response/compute/v2_26/servers.py
@@ -43,6 +43,25 @@
list_servers_detail['response_body']['properties']['servers']['items'][
'required'].append('tags')
+update_server = copy.deepcopy(servers219.update_server)
+update_server['response_body']['properties']['server'][
+ 'properties'].update({'tags': tag_items})
+update_server['response_body']['properties']['server'][
+ 'required'].append('tags')
+
+rebuild_server = copy.deepcopy(servers219.rebuild_server)
+rebuild_server['response_body']['properties']['server'][
+ 'properties'].update({'tags': tag_items})
+rebuild_server['response_body']['properties']['server'][
+ 'required'].append('tags')
+
+rebuild_server_with_admin_pass = copy.deepcopy(
+ servers219.rebuild_server_with_admin_pass)
+rebuild_server_with_admin_pass['response_body']['properties']['server'][
+ 'properties'].update({'tags': tag_items})
+rebuild_server_with_admin_pass['response_body']['properties']['server'][
+ 'required'].append('tags')
+
# list response schema wasn't changed for v2.26 so use v2.1
list_servers = copy.deepcopy(servers21.list_servers)
diff --git a/tempest/lib/api_schema/response/compute/v2_47/servers.py b/tempest/lib/api_schema/response/compute/v2_47/servers.py
index 37a084f..935be70 100644
--- a/tempest/lib/api_schema/response/compute/v2_47/servers.py
+++ b/tempest/lib/api_schema/response/compute/v2_47/servers.py
@@ -37,3 +37,19 @@
get_server = copy.deepcopy(servers226.get_server)
get_server['response_body']['properties']['server'][
'properties'].update({'flavor': flavor})
+list_servers_detail = copy.deepcopy(servers226.list_servers_detail)
+list_servers_detail['response_body']['properties']['servers']['items'][
+ 'properties'].update({'flavor': flavor})
+
+update_server = copy.deepcopy(servers226.update_server)
+update_server['response_body']['properties']['server'][
+ 'properties'].update({'flavor': flavor})
+
+rebuild_server = copy.deepcopy(servers226.rebuild_server)
+rebuild_server['response_body']['properties']['server'][
+ 'properties'].update({'flavor': flavor})
+
+rebuild_server_with_admin_pass = copy.deepcopy(
+ servers226.rebuild_server_with_admin_pass)
+rebuild_server_with_admin_pass['response_body']['properties']['server'][
+ 'properties'].update({'flavor': flavor})
diff --git a/tempest/lib/api_schema/response/compute/v2_54/__init__.py b/tempest/lib/api_schema/response/compute/v2_54/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_54/__init__.py
diff --git a/tempest/lib/api_schema/response/compute/v2_54/servers.py b/tempest/lib/api_schema/response/compute/v2_54/servers.py
new file mode 100644
index 0000000..c084696
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_54/servers.py
@@ -0,0 +1,49 @@
+# 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 copy
+
+from tempest.lib.api_schema.response.compute.v2_47 import servers as servers247
+# ****** Schemas changed in microversion 2.54 *****************
+
+# Note(gmann): This is schema for microversion 2.54 which includes the
+# 'key_name' in the Response body of the following APIs:
+# - ``POST '/servers/{server_id}/action (rebuild)``
+
+key_name = {
+ 'oneOf': [
+ {'type': 'string', 'minLength': 1, 'maxLength': 255},
+ {'type': 'null'},
+ ]
+}
+
+rebuild_server = copy.deepcopy(servers247.rebuild_server)
+rebuild_server['response_body']['properties']['server'][
+ 'properties'].update({'key_name': key_name})
+rebuild_server['response_body']['properties']['server'][
+ 'required'].append('key_name')
+
+rebuild_server_with_admin_pass = copy.deepcopy(
+ servers247.rebuild_server_with_admin_pass)
+rebuild_server_with_admin_pass['response_body']['properties']['server'][
+ 'properties'].update({'key_name': key_name})
+rebuild_server_with_admin_pass['response_body']['properties']['server'][
+ 'required'].append('key_name')
+
+# ****** Schemas unchanged in microversion 2.54 since microversion 2.47 ***
+
+# NOTE(gmann): Below are the unchanged schema in this microversion. We need
+# to keep this schema in this file to have the generic way to select the
+# right schema based on self.schema_versions_info mapping in service client.
+get_server = copy.deepcopy(servers247.get_server)
+list_servers_detail = copy.deepcopy(servers247.list_servers_detail)
+update_server = copy.deepcopy(servers247.update_server)
diff --git a/tempest/lib/api_schema/response/compute/v2_55/__init__.py b/tempest/lib/api_schema/response/compute/v2_55/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_55/__init__.py
diff --git a/tempest/lib/api_schema/response/compute/v2_55/flavors.py b/tempest/lib/api_schema/response/compute/v2_55/flavors.py
new file mode 100644
index 0000000..823190a
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_55/flavors.py
@@ -0,0 +1,112 @@
+# Copyright 2018 NEC Corporation. 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.lib.api_schema.response.compute.v2_1 import parameter_types
+
+# Note(gmann): This is schema for microversion 2.55 which includes the
+# following changes:
+# Add new PUT API
+# Adds a ``description`` field to the following APIs response:
+# - ``GET /flavors``
+# - ``GET /flavors/detail``
+# - ``GET /flavors/{flavor_id}``
+# - ``POST /flavors``
+
+flavor_description = {
+ 'type': ['string', 'null'],
+ 'minLength': 0, 'maxLength': 65535
+}
+
+list_flavors = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'flavors': {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'name': {'type': 'string'},
+ 'links': parameter_types.links,
+ 'id': {'type': 'string'},
+ 'description': flavor_description
+ },
+ 'additionalProperties': False,
+ 'required': ['name', 'links', 'id', 'description']
+ }
+ },
+ 'flavors_links': parameter_types.links
+ },
+ 'additionalProperties': False,
+ # NOTE(gmann): flavors_links attribute is not necessary
+ # to be present always So it is not 'required'.
+ 'required': ['flavors']
+ }
+}
+
+common_flavor_info = {
+ 'type': 'object',
+ 'properties': {
+ 'name': {'type': 'string'},
+ 'links': parameter_types.links,
+ 'ram': {'type': 'integer'},
+ 'vcpus': {'type': 'integer'},
+ # 'swap' attributes comes as integer value but if it is empty
+ # it comes as "". So defining type of as string and integer.
+ 'swap': {'type': ['integer', 'string']},
+ 'disk': {'type': 'integer'},
+ 'id': {'type': 'string'},
+ 'OS-FLV-DISABLED:disabled': {'type': 'boolean'},
+ 'os-flavor-access:is_public': {'type': 'boolean'},
+ 'rxtx_factor': {'type': 'number'},
+ 'OS-FLV-EXT-DATA:ephemeral': {'type': 'integer'},
+ 'description': flavor_description
+ },
+ 'additionalProperties': False,
+ # 'OS-FLV-DISABLED', 'os-flavor-access', 'rxtx_factor' and
+ # 'OS-FLV-EXT-DATA' are API extensions. So they are not 'required'.
+ 'required': ['name', 'links', 'ram', 'vcpus', 'swap', 'disk', 'id',
+ 'description']
+}
+
+list_flavors_details = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'flavors': {
+ 'type': 'array',
+ 'items': common_flavor_info
+ },
+ # NOTE(gmann): flavors_links attribute is not necessary
+ # to be present always So it is not 'required'.
+ 'flavors_links': parameter_types.links
+ },
+ 'additionalProperties': False,
+ 'required': ['flavors']
+ }
+}
+
+create_update_get_flavor_details = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'flavor': common_flavor_info
+ },
+ 'additionalProperties': False,
+ 'required': ['flavor']
+ }
+}
diff --git a/tempest/lib/api_schema/response/compute/v2_9/servers.py b/tempest/lib/api_schema/response/compute/v2_9/servers.py
index e260e48..7df02d5 100644
--- a/tempest/lib/api_schema/response/compute/v2_9/servers.py
+++ b/tempest/lib/api_schema/response/compute/v2_9/servers.py
@@ -14,6 +14,7 @@
import copy
+from tempest.lib.api_schema.response.compute.v2_1 import servers as servers_21
from tempest.lib.api_schema.response.compute.v2_6 import servers
list_servers = copy.deepcopy(servers.list_servers)
@@ -29,3 +30,22 @@
'properties'].update({'locked': {'type': 'boolean'}})
list_servers_detail['response_body']['properties']['servers']['items'][
'required'].append('locked')
+
+update_server = copy.deepcopy(servers_21.update_server)
+update_server['response_body']['properties']['server'][
+ 'properties'].update({'locked': {'type': 'boolean'}})
+update_server['response_body']['properties']['server'][
+ 'required'].append('locked')
+
+rebuild_server = copy.deepcopy(servers_21.rebuild_server)
+rebuild_server['response_body']['properties']['server'][
+ 'properties'].update({'locked': {'type': 'boolean'}})
+rebuild_server['response_body']['properties']['server'][
+ 'required'].append('locked')
+
+rebuild_server_with_admin_pass = copy.deepcopy(
+ servers_21.rebuild_server_with_admin_pass)
+rebuild_server_with_admin_pass['response_body']['properties']['server'][
+ 'properties'].update({'locked': {'type': 'boolean'}})
+rebuild_server_with_admin_pass['response_body']['properties']['server'][
+ 'required'].append('locked')
diff --git a/tempest/lib/base.py b/tempest/lib/base.py
index 33a32ee..3be55c0 100644
--- a/tempest/lib/base.py
+++ b/tempest/lib/base.py
@@ -43,8 +43,7 @@
super(BaseTestCase, self).setUp()
if not self.setUpClassCalled:
raise RuntimeError("setUpClass does not calls the super's "
- "setUpClass in the "
- + self.__class__.__name__)
+ "setUpClass in {!r}".format(type(self)))
test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
try:
test_timeout = int(test_timeout)
@@ -62,7 +61,7 @@
stderr = self.useFixture(fixtures.StringStream('stderr')).stream
self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
if (os.environ.get('OS_LOG_CAPTURE') != 'False' and
- os.environ.get('OS_LOG_CAPTURE') != '0'):
+ os.environ.get('OS_LOG_CAPTURE') != '0'):
self.useFixture(fixtures.LoggerFixture(nuke_handlers=False,
format=self.log_format,
level=None))
diff --git a/tempest/lib/cli/output_parser.py b/tempest/lib/cli/output_parser.py
index 2edd5c1..a7d5e49 100644
--- a/tempest/lib/cli/output_parser.py
+++ b/tempest/lib/cli/output_parser.py
@@ -37,8 +37,8 @@
items = []
tables_ = tables(output_lines)
for table_ in tables_:
- if ('Property' not in table_['headers']
- or 'Value' not in table_['headers']):
+ if ('Property' not in table_['headers'] or
+ 'Value' not in table_['headers']):
raise exceptions.InvalidStructure()
item = {}
for value in table_['values']:
diff --git a/tempest/lib/cmd/check_uuid.py b/tempest/lib/cmd/check_uuid.py
index 101d692..d1f0888 100755
--- a/tempest/lib/cmd/check_uuid.py
+++ b/tempest/lib/cmd/check_uuid.py
@@ -103,7 +103,7 @@
def _modules_search(self):
"""Recursive search for python modules in base package"""
modules = []
- for root, dirs, files in os.walk(self.base_path):
+ for root, _, files in os.walk(self.base_path):
if not os.path.exists(os.path.join(root, '__init__.py')):
continue
root_package = self._path_to_package(root)
@@ -121,10 +121,10 @@
idempotent_id = None
for decorator in test_node.decorator_list:
if (hasattr(decorator, 'func') and
- hasattr(decorator.func, 'attr') and
- decorator.func.attr == DECORATOR_NAME and
- hasattr(decorator.func, 'value') and
- decorator.func.value.id == DECORATOR_MODULE):
+ hasattr(decorator.func, 'attr') and
+ decorator.func.attr == DECORATOR_NAME and
+ hasattr(decorator.func, 'value') and
+ decorator.func.value.id == DECORATOR_MODULE):
for arg in decorator.args:
idempotent_id = ast.literal_eval(arg)
return idempotent_id
@@ -165,8 +165,8 @@
@staticmethod
def _is_test_method(node):
- return (node.__class__ is ast.FunctionDef
- and node.name.startswith('test_'))
+ return (node.__class__ is ast.FunctionDef and
+ node.name.startswith('test_'))
@staticmethod
def _next_node(body, node):
diff --git a/tempest/lib/common/dynamic_creds.py b/tempest/lib/common/dynamic_creds.py
index 4f1a883..f27e926 100644
--- a/tempest/lib/common/dynamic_creds.py
+++ b/tempest/lib/common/dynamic_creds.py
@@ -338,15 +338,15 @@
credentials = self._create_creds(roles=credential_type)
self._creds[str(credential_type)] = credentials
# Maintained until tests are ported
- LOG.info("Acquired dynamic creds:\n credentials: %s", credentials)
- if (self.neutron_available and
- self.create_networks):
+ LOG.info("Acquired dynamic creds:\n"
+ " credentials: %s", credentials)
+ if (self.neutron_available and self.create_networks):
network, subnet, router = self._create_network_resources(
credentials.tenant_id)
credentials.set_resources(network=network, subnet=subnet,
router=router)
- LOG.info("Created isolated network resources for : \n"
- + " credentials: %s", credentials)
+ LOG.info("Created isolated network resources for:\n"
+ " credentials: %s", credentials)
return credentials
def get_primary_creds(self):
diff --git a/tempest/lib/services/compute/flavors_client.py b/tempest/lib/services/compute/flavors_client.py
index 0fb1991..4923d7e 100644
--- a/tempest/lib/services/compute/flavors_client.py
+++ b/tempest/lib/services/compute/flavors_client.py
@@ -21,12 +21,18 @@
as schema_access
from tempest.lib.api_schema.response.compute.v2_1 import flavors_extra_specs \
as schema_extra_specs
+from tempest.lib.api_schema.response.compute.v2_55 import flavors \
+ as schemav255
from tempest.lib.common import rest_client
from tempest.lib.services.compute import base_compute_client
class FlavorsClient(base_compute_client.BaseComputeClient):
+ schema_versions_info = [
+ {'min': None, 'max': '2.54', 'schema': schema},
+ {'min': '2.55', 'max': None, 'schema': schemav255}]
+
def list_flavors(self, detail=False, **params):
"""Lists flavors.
@@ -36,11 +42,12 @@
https://developer.openstack.org/api-ref/compute/#list-flavors-with-details
"""
url = 'flavors'
- _schema = schema.list_flavors
-
+ schema = self.get_schema(self.schema_versions_info)
if detail:
url += '/detail'
_schema = schema.list_flavors_details
+ else:
+ _schema = schema.list_flavors
if params:
url += '?%s' % urllib.urlencode(params)
@@ -58,7 +65,9 @@
"""
resp, body = self.get("flavors/%s" % flavor_id)
body = json.loads(body)
- self.validate_response(schema.create_get_flavor_details, resp, body)
+ schema = self.get_schema(self.schema_versions_info)
+ self.validate_response(schema.create_update_get_flavor_details,
+ resp, body)
return rest_client.ResponseBody(resp, body)
def create_flavor(self, **kwargs):
@@ -77,7 +86,25 @@
resp, body = self.post('flavors', post_body)
body = json.loads(body)
- self.validate_response(schema.create_get_flavor_details, resp, body)
+ schema = self.get_schema(self.schema_versions_info)
+ self.validate_response(schema.create_update_get_flavor_details,
+ resp, body)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_flavor(self, flavor_id, **kwargs):
+ """Uodate the flavor or instance type.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/compute/#update-flavor-description
+ """
+ put_body = json.dumps({'flavor': kwargs})
+ resp, body = self.put("flavors/%s" % flavor_id, put_body)
+
+ body = json.loads(body)
+ schema = self.get_schema(self.schema_versions_info)
+ self.validate_response(schema.create_update_get_flavor_details,
+ resp, body)
return rest_client.ResponseBody(resp, body)
def delete_flavor(self, flavor_id):
diff --git a/tempest/lib/services/compute/servers_client.py b/tempest/lib/services/compute/servers_client.py
index 09bccab..e75cdb5 100644
--- a/tempest/lib/services/compute/servers_client.py
+++ b/tempest/lib/services/compute/servers_client.py
@@ -29,6 +29,7 @@
from tempest.lib.api_schema.response.compute.v2_3 import servers as schemav23
from tempest.lib.api_schema.response.compute.v2_47 import servers as schemav247
from tempest.lib.api_schema.response.compute.v2_48 import servers as schemav248
+from tempest.lib.api_schema.response.compute.v2_54 import servers as schemav254
from tempest.lib.api_schema.response.compute.v2_6 import servers as schemav26
from tempest.lib.api_schema.response.compute.v2_9 import servers as schemav29
from tempest.lib.common import rest_client
@@ -47,7 +48,8 @@
{'min': '2.19', 'max': '2.25', 'schema': schemav219},
{'min': '2.26', 'max': '2.46', 'schema': schemav226},
{'min': '2.47', 'max': '2.47', 'schema': schemav247},
- {'min': '2.48', 'max': None, 'schema': schemav248}]
+ {'min': '2.48', 'max': '2.53', 'schema': schemav248},
+ {'min': '2.54', 'max': None, 'schema': schemav254}]
def __init__(self, auth_provider, service, region,
enable_instance_password=True, **kwargs):
@@ -156,11 +158,11 @@
url = 'servers'
schema = self.get_schema(self.schema_versions_info)
- _schema = schema.list_servers
-
if detail:
url += '/detail'
_schema = schema.list_servers_detail
+ else:
+ _schema = schema.list_servers
if params:
url += '?%s' % urllib.urlencode(params)
diff --git a/tempest/lib/services/identity/v3/__init__.py b/tempest/lib/services/identity/v3/__init__.py
index a539d08..da1c51c 100644
--- a/tempest/lib/services/identity/v3/__init__.py
+++ b/tempest/lib/services/identity/v3/__init__.py
@@ -12,6 +12,8 @@
# License for the specific language governing permissions and limitations under
# the License.
+from tempest.lib.services.identity.v3.application_credentials_client import \
+ ApplicationCredentialsClient
from tempest.lib.services.identity.v3.catalog_client import \
CatalogClient
from tempest.lib.services.identity.v3.credentials_client import \
@@ -33,6 +35,8 @@
from tempest.lib.services.identity.v3.oauth_token_client import \
OAUTHTokenClient
from tempest.lib.services.identity.v3.policies_client import PoliciesClient
+from tempest.lib.services.identity.v3.project_tags_client import \
+ ProjectTagsClient
from tempest.lib.services.identity.v3.projects_client import ProjectsClient
from tempest.lib.services.identity.v3.regions_client import RegionsClient
from tempest.lib.services.identity.v3.role_assignments_client import \
@@ -44,11 +48,11 @@
from tempest.lib.services.identity.v3.users_client import UsersClient
from tempest.lib.services.identity.v3.versions_client import VersionsClient
-__all__ = ['CatalogClient', 'CredentialsClient', 'DomainsClient',
- 'DomainConfigurationClient', 'EndPointGroupsClient',
- 'EndPointsClient', 'EndPointsFilterClient', 'GroupsClient',
- 'IdentityClient', 'InheritedRolesClient', 'OAUTHConsumerClient',
- 'OAUTHTokenClient', 'PoliciesClient', 'ProjectsClient',
- 'RegionsClient', 'RoleAssignmentsClient', 'RolesClient',
- 'ServicesClient', 'V3TokenClient', 'TrustsClient', 'UsersClient',
- 'VersionsClient']
+__all__ = ['ApplicationCredentialsClient', 'CatalogClient',
+ 'CredentialsClient', 'DomainsClient', 'DomainConfigurationClient',
+ 'EndPointGroupsClient', 'EndPointsClient', 'EndPointsFilterClient',
+ 'GroupsClient', 'IdentityClient', 'InheritedRolesClient',
+ 'OAUTHConsumerClient', 'OAUTHTokenClient', 'PoliciesClient',
+ 'ProjectsClient', 'ProjectTagsClient', 'RegionsClient',
+ 'RoleAssignmentsClient', 'RolesClient', 'ServicesClient',
+ 'V3TokenClient', 'TrustsClient', 'UsersClient', 'VersionsClient']
diff --git a/tempest/lib/services/identity/v3/application_credentials_client.py b/tempest/lib/services/identity/v3/application_credentials_client.py
new file mode 100644
index 0000000..557aa9e
--- /dev/null
+++ b/tempest/lib/services/identity/v3/application_credentials_client.py
@@ -0,0 +1,83 @@
+# Copyright 2018 SUSE Linux GmbH
+#
+# 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.
+
+"""
+https://developer.openstack.org/api-ref/identity/v3/index.html#application-credentials
+"""
+
+from oslo_serialization import jsonutils as json
+from six.moves.urllib import parse as urllib
+
+from tempest.lib.common import rest_client
+
+
+class ApplicationCredentialsClient(rest_client.RestClient):
+ api_version = "v3"
+
+ def create_application_credential(self, user_id, **kwargs):
+ """Creates an application credential.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/identity/v3/index.html#create-application-credential
+ """
+ post_body = json.dumps({'application_credential': kwargs})
+ resp, body = self.post('users/%s/application_credentials' % user_id,
+ post_body)
+ self.expected_success(201, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_application_credential(self, user_id, application_credential_id):
+ """Gets details of an application credential.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/identity/v3/index.html#show-application-credential-details
+ """
+ resp, body = self.get('users/%s/application_credentials/%s' %
+ (user_id, application_credential_id))
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_application_credentials(self, user_id, **params):
+ """Lists out all of a user's application credentials.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/identity/v3/index.html#list-application-credentials
+ """
+ url = 'users/%s/application_credentials' % user_id
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+ resp, body = self.get(url)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_application_credential(self, user_id,
+ application_credential_id):
+ """Deletes an application credential.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/identity/v3/index.html#delete-application-credential
+ """
+ resp, body = self.delete('users/%s/application_credentials/%s' %
+ (user_id, application_credential_id))
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/identity/v3/project_tags_client.py b/tempest/lib/services/identity/v3/project_tags_client.py
new file mode 100644
index 0000000..dd1a2a5
--- /dev/null
+++ b/tempest/lib/services/identity/v3/project_tags_client.py
@@ -0,0 +1,80 @@
+# Copyright 2018 AT&T Corporation.
+# 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 oslo_serialization import jsonutils as json
+
+from tempest.lib.common import rest_client
+
+
+class ProjectTagsClient(rest_client.RestClient):
+ api_version = "v3"
+
+ def update_project_tag(self, project_id, tag):
+ """Updates the specified tag and adds it to the project's list of tags.
+
+ """
+ url = 'projects/%s/tags/%s' % (project_id, tag)
+ resp, body = self.put(url, '{}')
+ # NOTE(felipemonteiro): This API endpoint returns 201 AND an empty
+ # response body, which is consistent with the spec:
+ # https://specs.openstack.org/openstack/api-wg/guidelines/tags.html#addressing-individual-tags
+ self.expected_success(201, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_project_tags(self, project_id):
+ """List tags for a project."""
+ url = "projects/%s/tags" % project_id
+ resp, body = self.get(url)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_all_project_tags(self, project_id, tags, **kwargs):
+ """Updates all the tags for a project.
+
+ Any existing tags not specified will be deleted.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/identity/v3/#modify-tag-list-for-a-project
+ """
+ body = {'tags': tags}
+ if kwargs:
+ body.update(kwargs)
+ put_body = json.dumps(body)
+ resp, body = self.put('projects/%s/tags' % project_id, put_body)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def check_project_tag_existence(self, project_id, tag):
+ """Check if a project contains a tag."""
+ url = 'projects/%s/tags/%s' % (project_id, tag)
+ resp, body = self.get(url)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_project_tag(self, project_id, tag):
+ """Delete a project tag."""
+ url = 'projects/%s/tags/%s' % (project_id, tag)
+ resp, body = self.delete(url)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_all_project_tags(self, project_id):
+ """Delete all tags from a project."""
+ resp, body = self.delete('projects/%s/tags' % project_id)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/identity/v3/token_client.py b/tempest/lib/services/identity/v3/token_client.py
index 33f6f16..d591f03 100644
--- a/tempest/lib/services/identity/v3/token_client.py
+++ b/tempest/lib/services/identity/v3/token_client.py
@@ -51,7 +51,8 @@
def auth(self, user_id=None, username=None, password=None, project_id=None,
project_name=None, user_domain_id=None, user_domain_name=None,
project_domain_id=None, project_domain_name=None, domain_id=None,
- domain_name=None, token=None):
+ domain_name=None, token=None, app_cred_id=None,
+ app_cred_secret=None):
"""Obtains a token from the authentication service
:param user_id: user id
@@ -109,6 +110,13 @@
if _domain:
id_obj['password']['user']['domain'] = _domain
+ if app_cred_id and app_cred_secret:
+ id_obj['methods'].append('application_credential')
+ id_obj['application_credential'] = {
+ 'id': app_cred_id,
+ 'secret': app_cred_secret,
+ }
+
if (project_id or project_name):
_project = dict()
diff --git a/tempest/lib/services/volume/v2/availability_zone_client.py b/tempest/lib/services/volume/v2/availability_zone_client.py
index bb4a357..bdb2304 100644
--- a/tempest/lib/services/volume/v2/availability_zone_client.py
+++ b/tempest/lib/services/volume/v2/availability_zone_client.py
@@ -13,16 +13,11 @@
# License for the specific language governing permissions and limitations
# under the License.
-from oslo_serialization import jsonutils as json
+from debtcollector import moves
-from tempest.lib.common import rest_client
+from tempest.lib.services.volume.v3 import availability_zone_client
-class AvailabilityZoneClient(rest_client.RestClient):
- api_version = "v2"
-
- def list_availability_zones(self):
- resp, body = self.get('os-availability-zone')
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return rest_client.ResponseBody(resp, body)
+AvailabilityZoneClient = moves.moved_class(
+ availability_zone_client.AvailabilityZoneClient, 'AvailabilityZoneClient',
+ __name__, version="Rocky", removal_version='?')
diff --git a/tempest/lib/services/volume/v2/backups_client.py b/tempest/lib/services/volume/v2/backups_client.py
index adfa6a6..80b3631 100644
--- a/tempest/lib/services/volume/v2/backups_client.py
+++ b/tempest/lib/services/volume/v2/backups_client.py
@@ -12,108 +12,11 @@
# 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 debtcollector import moves
-from oslo_serialization import jsonutils as json
-from six.moves.urllib import parse as urllib
-
-from tempest.lib.common import rest_client
-from tempest.lib import exceptions as lib_exc
-from tempest.lib.services.volume import base_client
+from tempest.lib.services.volume.v3 import backups_client
-class BackupsClient(base_client.BaseClient):
- """Volume V2 Backups client"""
- api_version = "v2"
-
- def create_backup(self, **kwargs):
- """Creates a backup of volume.
-
- For a full list of available parameters, please refer to the official
- API reference:
- https://developer.openstack.org/api-ref/block-storage/v2/index.html#create-backup
- """
- post_body = json.dumps({'backup': kwargs})
- resp, body = self.post('backups', post_body)
- body = json.loads(body)
- self.expected_success(202, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def restore_backup(self, backup_id, **kwargs):
- """Restore volume from backup.
-
- For a full list of available parameters, please refer to the official
- API reference:
- https://developer.openstack.org/api-ref/block-storage/v2/index.html#restore-backup
- """
- post_body = json.dumps({'restore': kwargs})
- resp, body = self.post('backups/%s/restore' % (backup_id), post_body)
- body = json.loads(body)
- self.expected_success(202, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def delete_backup(self, backup_id):
- """Delete a backup of volume."""
- resp, body = self.delete('backups/%s' % backup_id)
- self.expected_success(202, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def show_backup(self, backup_id):
- """Returns the details of a single backup."""
- url = "backups/%s" % backup_id
- resp, body = self.get(url)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def list_backups(self, detail=False, **params):
- """List all the tenant's backups.
-
- For a full list of available parameters, please refer to the official
- API reference:
- http://developer.openstack.org/api-ref/block-storage/v2/#list-backups
- http://developer.openstack.org/api-ref/block-storage/v2/#list-backups-with-details
- """
- url = "backups"
- if detail:
- url += "/detail"
- if params:
- url += '?%s' % urllib.urlencode(params)
- resp, body = self.get(url)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def export_backup(self, backup_id):
- """Export backup metadata record."""
- url = "backups/%s/export_record" % backup_id
- resp, body = self.get(url)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def import_backup(self, **kwargs):
- """Import backup metadata record."""
- post_body = json.dumps({'backup-record': kwargs})
- resp, body = self.post("backups/import_record", post_body)
- body = json.loads(body)
- self.expected_success(201, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def reset_backup_status(self, backup_id, status):
- """Reset the specified backup's status."""
- post_body = json.dumps({'os-reset_status': {"status": status}})
- resp, body = self.post('backups/%s/action' % backup_id, post_body)
- self.expected_success(202, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def is_resource_deleted(self, id):
- try:
- self.show_backup(id)
- except lib_exc.NotFound:
- return True
- return False
-
- @property
- def resource_type(self):
- """Returns the primary type of resource this client works with."""
- return 'backup'
+BackupsClient = moves.moved_class(
+ backups_client.BackupsClient, 'BackupsClient',
+ __name__, version="Rocky", removal_version='?')
diff --git a/tempest/lib/services/volume/v2/capabilities_client.py b/tempest/lib/services/volume/v2/capabilities_client.py
index 240be13..d8cf806 100644
--- a/tempest/lib/services/volume/v2/capabilities_client.py
+++ b/tempest/lib/services/volume/v2/capabilities_client.py
@@ -13,23 +13,11 @@
# License for the specific language governing permissions and limitations
# under the License.
-from oslo_serialization import jsonutils as json
+from debtcollector import moves
-from tempest.lib.common import rest_client
+from tempest.lib.services.volume.v3 import capabilities_client
-class CapabilitiesClient(rest_client.RestClient):
- api_version = "v2"
-
- def show_backend_capabilities(self, host):
- """Shows capabilities for a storage back end.
-
- For a full list of available parameters, please refer to the official
- API reference:
- https://developer.openstack.org/api-ref/block-storage/v2/index.html#show-back-end-capabilities
- """
- url = 'capabilities/%s' % host
- resp, body = self.get(url)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return rest_client.ResponseBody(resp, body)
+CapabilitiesClient = moves.moved_class(
+ capabilities_client.CapabilitiesClient, 'CapabilitiesClient',
+ __name__, version="Queens", removal_version='?')
diff --git a/tempest/lib/services/volume/v2/encryption_types_client.py b/tempest/lib/services/volume/v2/encryption_types_client.py
index b99d1fe..875e59e 100644
--- a/tempest/lib/services/volume/v2/encryption_types_client.py
+++ b/tempest/lib/services/volume/v2/encryption_types_client.py
@@ -13,79 +13,11 @@
# License for the specific language governing permissions and limitations
# under the License.
-from oslo_serialization import jsonutils as json
+from debtcollector import moves
-from tempest.lib.common import rest_client
-from tempest.lib import exceptions as lib_exc
+from tempest.lib.services.volume.v3 import encryption_types_client
-class EncryptionTypesClient(rest_client.RestClient):
- api_version = "v2"
-
- def is_resource_deleted(self, id):
- try:
- body = self.show_encryption_type(id)
- if not body:
- return True
- except lib_exc.NotFound:
- return True
- return False
-
- @property
- def resource_type(self):
- """Returns the primary type of resource this client works with."""
- return 'encryption-type'
-
- def show_encryption_type(self, volume_type_id):
- """Get the volume encryption type for the specified volume type.
-
- volume_type_id: Id of volume_type.
- """
- url = "/types/%s/encryption" % volume_type_id
- resp, body = self.get(url)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def show_encryption_specs_item(self, volume_type_id, key):
- """Get the encryption specs item for the specified volume type."""
- url = "/types/%s/encryption/%s" % (volume_type_id, key)
- resp, body = self.get(url)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def create_encryption_type(self, volume_type_id, **kwargs):
- """Create encryption type.
-
- For a full list of available parameters, please refer to the official
- API reference:
- https://developer.openstack.org/api-ref/block-storage/v2/#create-an-encryption-type-for-v2
- """
- url = "/types/%s/encryption" % volume_type_id
- post_body = json.dumps({'encryption': kwargs})
- resp, body = self.post(url, post_body)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def delete_encryption_type(self, volume_type_id):
- """Delete the encryption type for the specified volume-type."""
- resp, body = self.delete(
- "/types/%s/encryption/provider" % volume_type_id)
- self.expected_success(202, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def update_encryption_type(self, volume_type_id, **kwargs):
- """Update an encryption type for an existing volume type.
-
- For a full list of available parameters, please refer to the official
- API reference:
- https://developer.openstack.org/api-ref/block-storage/v2/#update-an-encryption-type-for-v2
- """
- url = "/types/%s/encryption/provider" % volume_type_id
- put_body = json.dumps({'encryption': kwargs})
- resp, body = self.put(url, put_body)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return rest_client.ResponseBody(resp, body)
+EncryptionTypesClient = moves.moved_class(
+ encryption_types_client.EncryptionTypesClient, 'EncryptionTypesClient',
+ __name__, version="Rocky", removal_version='?')
diff --git a/tempest/lib/services/volume/v2/extensions_client.py b/tempest/lib/services/volume/v2/extensions_client.py
index 09279d5..6316ef5 100644
--- a/tempest/lib/services/volume/v2/extensions_client.py
+++ b/tempest/lib/services/volume/v2/extensions_client.py
@@ -12,19 +12,11 @@
# 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 debtcollector import moves
-from oslo_serialization import jsonutils as json
-
-from tempest.lib.common import rest_client
+from tempest.lib.services.volume.v3 import extensions_client
-class ExtensionsClient(rest_client.RestClient):
- """Volume V2 extensions client."""
- api_version = "v2"
-
- def list_extensions(self):
- url = 'extensions'
- resp, body = self.get(url)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return rest_client.ResponseBody(resp, body)
+ExtensionsClient = moves.moved_class(
+ extensions_client.ExtensionsClient, 'ExtensionsClient',
+ __name__, version="Rocky", removal_version='?')
diff --git a/tempest/lib/services/volume/v2/hosts_client.py b/tempest/lib/services/volume/v2/hosts_client.py
index f44bda3..38f1b38 100644
--- a/tempest/lib/services/volume/v2/hosts_client.py
+++ b/tempest/lib/services/volume/v2/hosts_client.py
@@ -12,37 +12,11 @@
# 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 debtcollector import moves
-from oslo_serialization import jsonutils as json
-from six.moves.urllib import parse as urllib
-
-from tempest.lib.common import rest_client
+from tempest.lib.services.volume.v3 import hosts_client
-class HostsClient(rest_client.RestClient):
- """Client class to send CRUD Volume V2 API requests"""
- api_version = "v2"
-
- def list_hosts(self, **params):
- """Lists all hosts.
-
- For a full list of available parameters, please refer to the official
- API reference:
- https://developer.openstack.org/api-ref/block-storage/v2/#list-all-hosts
- """
- url = 'os-hosts'
- if params:
- url += '?%s' % urllib.urlencode(params)
-
- resp, body = self.get(url)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def show_host(self, host_name):
- """Show host details."""
- url = 'os-hosts/%s' % host_name
- resp, body = self.get(url)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return rest_client.ResponseBody(resp, body)
+HostsClient = moves.moved_class(
+ hosts_client.HostsClient, 'HostsClient',
+ __name__, version="Rocky", removal_version='?')
diff --git a/tempest/lib/services/volume/v2/limits_client.py b/tempest/lib/services/volume/v2/limits_client.py
index ce9fba9..a6b8c5a 100644
--- a/tempest/lib/services/volume/v2/limits_client.py
+++ b/tempest/lib/services/volume/v2/limits_client.py
@@ -12,21 +12,11 @@
# 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 debtcollector import moves
-from oslo_serialization import jsonutils as json
-
-from tempest.lib.common import rest_client
+from tempest.lib.services.volume.v3 import limits_client
-class LimitsClient(rest_client.RestClient):
- """Volume V2 limits client."""
-
- api_version = "v2"
-
- def show_limits(self):
- """Returns the details of a volume absolute limits."""
- url = "limits"
- resp, body = self.get(url)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return rest_client.ResponseBody(resp, body)
+LimitsClient = moves.moved_class(
+ limits_client.LimitsClient, 'LimitsClient',
+ __name__, version="Rocky", removal_version='?')
diff --git a/tempest/lib/services/volume/v2/qos_client.py b/tempest/lib/services/volume/v2/qos_client.py
index 47d3914..b81384e 100644
--- a/tempest/lib/services/volume/v2/qos_client.py
+++ b/tempest/lib/services/volume/v2/qos_client.py
@@ -11,123 +11,11 @@
# 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 debtcollector import moves
-from oslo_serialization import jsonutils as json
-
-from tempest.lib.common import rest_client
-from tempest.lib import exceptions as lib_exc
+from tempest.lib.services.volume.v3 import qos_client
-class QosSpecsClient(rest_client.RestClient):
- """Volume V2 QoS client.
-
- Client class to send CRUD QoS API requests
- """
-
- api_version = "v2"
-
- def is_resource_deleted(self, qos_id):
- try:
- self.show_qos(qos_id)
- except lib_exc.NotFound:
- return True
- return False
-
- @property
- def resource_type(self):
- """Returns the primary type of resource this client works with."""
- return 'qos'
-
- def create_qos(self, **kwargs):
- """Create a QoS Specification.
-
- For a full list of available parameters, please refer to the official
- API reference:
- http://developer.openstack.org/api-ref/block-storage/v2/#create-qos-specification
- """
- post_body = json.dumps({'qos_specs': kwargs})
- resp, body = self.post('qos-specs', post_body)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return rest_client.ResponseBody(resp, body)
-
- def delete_qos(self, qos_id, force=False):
- """Delete the specified QoS specification."""
- resp, body = self.delete(
- "qos-specs/%s?force=%s" % (qos_id, force))
- self.expected_success(202, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def list_qos(self):
- """List all the QoS specifications created."""
- url = 'qos-specs'
- resp, body = self.get(url)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def show_qos(self, qos_id):
- """Get the specified QoS specification."""
- url = "qos-specs/%s" % qos_id
- resp, body = self.get(url)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def set_qos_key(self, qos_id, **kwargs):
- """Set the specified keys/values of QoS specification.
-
- For a full list of available parameters, please refer to the official
- API reference:
- http://developer.openstack.org/api-ref/block-storage/v2/#set-keys-in-qos-specification
- """
- put_body = json.dumps({"qos_specs": kwargs})
- resp, body = self.put('qos-specs/%s' % qos_id, put_body)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def unset_qos_key(self, qos_id, keys):
- """Unset the specified keys of QoS specification.
-
- :param keys: keys to delete from the QoS specification.
-
- For a full list of available parameters, please refer to the official
- API reference:
- http://developer.openstack.org/api-ref/block-storage/v2/#unset-keys-in-qos-specification
- """
- put_body = json.dumps({'keys': keys})
- resp, body = self.put('qos-specs/%s/delete_keys' % qos_id, put_body)
- self.expected_success(202, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def associate_qos(self, qos_id, vol_type_id):
- """Associate the specified QoS with specified volume-type."""
- url = "qos-specs/%s/associate" % qos_id
- url += "?vol_type_id=%s" % vol_type_id
- resp, body = self.get(url)
- self.expected_success(202, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def show_association_qos(self, qos_id):
- """Get the association of the specified QoS specification."""
- url = "qos-specs/%s/associations" % qos_id
- resp, body = self.get(url)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def disassociate_qos(self, qos_id, vol_type_id):
- """Disassociate the specified QoS with specified volume-type."""
- url = "qos-specs/%s/disassociate" % qos_id
- url += "?vol_type_id=%s" % vol_type_id
- resp, body = self.get(url)
- self.expected_success(202, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def disassociate_all_qos(self, qos_id):
- """Disassociate the specified QoS with all associations."""
- url = "qos-specs/%s/disassociate_all" % qos_id
- resp, body = self.get(url)
- self.expected_success(202, resp.status)
- return rest_client.ResponseBody(resp, body)
+QosSpecsClient = moves.moved_class(
+ qos_client.QosSpecsClient, 'QosSpecsClient',
+ __name__, version="Rocky", removal_version='?')
diff --git a/tempest/lib/services/volume/v2/quota_classes_client.py b/tempest/lib/services/volume/v2/quota_classes_client.py
index 733b1ac..24aab89 100644
--- a/tempest/lib/services/volume/v2/quota_classes_client.py
+++ b/tempest/lib/services/volume/v2/quota_classes_client.py
@@ -12,40 +12,11 @@
# 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 debtcollector import moves
-from oslo_serialization import jsonutils as json
-
-from tempest.lib.common import rest_client
+from tempest.lib.services.volume.v3 import quota_classes_client
-class QuotaClassesClient(rest_client.RestClient):
- """Volume quota class V2 client."""
-
- api_version = "v2"
-
- def show_quota_class_set(self, quota_class_id):
- """List quotas for a quota class.
-
- For a full list of available parameters, please refer to the official
- API reference:
- https://developer.openstack.org/api-ref/block-storage/v2/index.html#show-quota-classes
- """
- url = 'os-quota-class-sets/%s' % quota_class_id
- resp, body = self.get(url)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return rest_client.ResponseBody(resp, body)
-
- def update_quota_class_set(self, quota_class_id, **kwargs):
- """Update quotas for a quota class.
-
- For a full list of available parameters, please refer to the official
- API reference:
- https://developer.openstack.org/api-ref/block-storage/v2/index.html#update-quota-classes
- """
- url = 'os-quota-class-sets/%s' % quota_class_id
- put_body = json.dumps({'quota_class_set': kwargs})
- resp, body = self.put(url, put_body)
- self.expected_success(200, resp.status)
- body = json.loads(body)
- return rest_client.ResponseBody(resp, body)
+QuotaClassesClient = moves.moved_class(
+ quota_classes_client.QuotaClassesClient, 'QuotaClassesClient',
+ __name__, version="Rocky", removal_version='?')
diff --git a/tempest/lib/services/volume/v2/quotas_client.py b/tempest/lib/services/volume/v2/quotas_client.py
index e4b2895..6f9f61c 100644
--- a/tempest/lib/services/volume/v2/quotas_client.py
+++ b/tempest/lib/services/volume/v2/quotas_client.py
@@ -12,53 +12,11 @@
# 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 debtcollector import moves
-from oslo_serialization import jsonutils
-from six.moves.urllib import parse as urllib
-
-from tempest.lib.common import rest_client
+from tempest.lib.services.volume.v3 import quotas_client
-class QuotasClient(rest_client.RestClient):
- """Client class to send CRUD Volume Quotas API V2 requests"""
- api_version = "v2"
-
- def show_default_quota_set(self, tenant_id):
- """List the default volume quota set for a tenant."""
-
- url = 'os-quota-sets/%s/defaults' % tenant_id
- resp, body = self.get(url)
- self.expected_success(200, resp.status)
- body = jsonutils.loads(body)
- return rest_client.ResponseBody(resp, body)
-
- def show_quota_set(self, tenant_id, params=None):
- """List the quota set for a tenant."""
-
- url = 'os-quota-sets/%s' % tenant_id
- if params:
- url += '?%s' % urllib.urlencode(params)
-
- resp, body = self.get(url)
- self.expected_success(200, resp.status)
- body = jsonutils.loads(body)
- return rest_client.ResponseBody(resp, body)
-
- def update_quota_set(self, tenant_id, **kwargs):
- """Updates quota set
-
- For a full list of available parameters, please refer to the official
- API reference:
- https://developer.openstack.org/api-ref/block-storage/v2/index.html#update-quotas
- """
- put_body = jsonutils.dumps({'quota_set': kwargs})
- resp, body = self.put('os-quota-sets/%s' % tenant_id, put_body)
- self.expected_success(200, resp.status)
- body = jsonutils.loads(body)
- return rest_client.ResponseBody(resp, body)
-
- def delete_quota_set(self, tenant_id):
- """Delete the tenant's quota set."""
- resp, body = self.delete('os-quota-sets/%s' % tenant_id)
- self.expected_success(200, resp.status)
- return rest_client.ResponseBody(resp, body)
+QuotasClient = moves.moved_class(
+ quotas_client.QuotasClient, 'QuotasClient',
+ __name__, version="Rocky", removal_version='?')
diff --git a/tempest/lib/services/volume/v2/scheduler_stats_client.py b/tempest/lib/services/volume/v2/scheduler_stats_client.py
index 0d04f85..a5adb34 100644
--- a/tempest/lib/services/volume/v2/scheduler_stats_client.py
+++ b/tempest/lib/services/volume/v2/scheduler_stats_client.py
@@ -12,26 +12,11 @@
# 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 debtcollector import moves
-from oslo_serialization import jsonutils as json
-
-from tempest.lib.common import rest_client
+from tempest.lib.services.volume.v3 import scheduler_stats_client
-class SchedulerStatsClient(rest_client.RestClient):
- api_version = "v2"
-
- def list_pools(self, detail=False):
- """List all the volumes pools (hosts).
-
- For a full list of available parameters, please refer to the official
- API reference:
- https://developer.openstack.org/api-ref/block-storage/v2/index.html#list-back-end-storage-pools
- """
- url = 'scheduler-stats/get_pools'
- if detail:
- url += '?detail=True'
- resp, body = self.get(url)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return rest_client.ResponseBody(resp, body)
+SchedulerStatsClient = moves.moved_class(
+ scheduler_stats_client.SchedulerStatsClient, 'SchedulerStatsClient',
+ __name__, version="Rocky", removal_version='?')
diff --git a/tempest/lib/services/volume/v2/services_client.py b/tempest/lib/services/volume/v2/services_client.py
index bc55469..a4491d3 100644
--- a/tempest/lib/services/volume/v2/services_client.py
+++ b/tempest/lib/services/volume/v2/services_client.py
@@ -12,23 +12,11 @@
# 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 debtcollector import moves
-from oslo_serialization import jsonutils as json
-from six.moves.urllib import parse as urllib
-
-from tempest.lib.common import rest_client
+from tempest.lib.services.volume.v3 import services_client
-class ServicesClient(rest_client.RestClient):
- """Client class to send CRUD Volume V2 API requests"""
- api_version = "v2"
-
- def list_services(self, **params):
- url = 'os-services'
- if params:
- url += '?%s' % urllib.urlencode(params)
-
- resp, body = self.get(url)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return rest_client.ResponseBody(resp, body)
+ServicesClient = moves.moved_class(
+ services_client.ServicesClient, 'ServicesClient',
+ __name__, version="Rocky", removal_version='?')
diff --git a/tempest/lib/services/volume/v2/snapshot_manage_client.py b/tempest/lib/services/volume/v2/snapshot_manage_client.py
index aecd30b..132209f 100644
--- a/tempest/lib/services/volume/v2/snapshot_manage_client.py
+++ b/tempest/lib/services/volume/v2/snapshot_manage_client.py
@@ -12,22 +12,11 @@
# 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 debtcollector import moves
-from oslo_serialization import jsonutils as json
-
-from tempest.lib.common import rest_client
+from tempest.lib.services.volume.v3 import snapshot_manage_client
-class SnapshotManageClient(rest_client.RestClient):
- """Snapshot manage V2 client."""
-
- api_version = "v2"
-
- def manage_snapshot(self, **kwargs):
- """Manage a snapshot."""
- post_body = json.dumps({'snapshot': kwargs})
- url = 'os-snapshot-manage'
- resp, body = self.post(url, post_body)
- self.expected_success(202, resp.status)
- body = json.loads(body)
- return rest_client.ResponseBody(resp, body)
+SnapshotManageClient = moves.moved_class(
+ snapshot_manage_client.SnapshotManageClient, 'SnapshotManageClient',
+ __name__, version="Rocky", removal_version='?')
diff --git a/tempest/lib/services/volume/v2/snapshots_client.py b/tempest/lib/services/volume/v2/snapshots_client.py
index 4bc2842..3a72cc1 100644
--- a/tempest/lib/services/volume/v2/snapshots_client.py
+++ b/tempest/lib/services/volume/v2/snapshots_client.py
@@ -9,200 +9,11 @@
# 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 debtcollector import moves
-from oslo_serialization import jsonutils as json
-from six.moves.urllib import parse as urllib
-
-from tempest.lib.common import rest_client
-from tempest.lib import exceptions as lib_exc
+from tempest.lib.services.volume.v3 import snapshots_client
-class SnapshotsClient(rest_client.RestClient):
- """Client class to send CRUD Volume V2 API requests."""
- api_version = "v2"
- create_resp = 202
-
- def list_snapshots(self, detail=False, **params):
- """List all the snapshot.
-
- For a full list of available parameters, please refer to the official
- API reference:
- http://developer.openstack.org/api-ref/block-storage/v2/#list-snapshots-with-details
- http://developer.openstack.org/api-ref/block-storage/v2/#list-snapshots
- """
- url = 'snapshots'
- if detail:
- url += '/detail'
- if params:
- url += '?%s' % urllib.urlencode(params)
-
- resp, body = self.get(url)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def show_snapshot(self, snapshot_id):
- """Returns the details of a single snapshot.
-
- For a full list of available parameters, please refer to the official
- API reference:
- http://developer.openstack.org/api-ref/block-storage/v2/#show-snapshot-details
- """
- url = "snapshots/%s" % snapshot_id
- resp, body = self.get(url)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def create_snapshot(self, **kwargs):
- """Creates a new snapshot.
-
- For a full list of available parameters, please refer to the official
- API reference:
- http://developer.openstack.org/api-ref/block-storage/v2/#create-snapshot
- """
- post_body = json.dumps({'snapshot': kwargs})
- resp, body = self.post('snapshots', post_body)
- body = json.loads(body)
- self.expected_success(self.create_resp, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def update_snapshot(self, snapshot_id, **kwargs):
- """Updates a snapshot.
-
- For a full list of available parameters, please refer to the official
- API reference:
- http://developer.openstack.org/api-ref/block-storage/v2/#update-snapshot
- """
- put_body = json.dumps({'snapshot': kwargs})
- resp, body = self.put('snapshots/%s' % snapshot_id, put_body)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def delete_snapshot(self, snapshot_id):
- """Delete Snapshot.
-
- For a full list of available parameters, please refer to the official
- API reference:
- http://developer.openstack.org/api-ref/block-storage/v2/#delete-snapshot
- """
- resp, body = self.delete("snapshots/%s" % snapshot_id)
- self.expected_success(202, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def is_resource_deleted(self, id):
- try:
- self.show_snapshot(id)
- except lib_exc.NotFound:
- return True
- return False
-
- @property
- def resource_type(self):
- """Returns the primary type of resource this client works with."""
- return 'volume-snapshot'
-
- def reset_snapshot_status(self, snapshot_id, status):
- """Reset the specified snapshot's status."""
- post_body = json.dumps({'os-reset_status': {"status": status}})
- resp, body = self.post('snapshots/%s/action' % snapshot_id, post_body)
- self.expected_success(202, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def update_snapshot_status(self, snapshot_id, **kwargs):
- """Update the specified snapshot's status."""
- # TODO(gmann): api-site doesn't contain doc ref
- # for this API. After fixing the api-site, we need to
- # add the link here.
- # Bug https://bugs.launchpad.net/openstack-api-site/+bug/1532645
-
- post_body = json.dumps({'os-update_snapshot_status': kwargs})
- url = 'snapshots/%s/action' % snapshot_id
- resp, body = self.post(url, post_body)
- self.expected_success(202, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def create_snapshot_metadata(self, snapshot_id, metadata):
- """Create metadata for the snapshot.
-
- For a full list of available parameters, please refer to the official
- API reference:
- http://developer.openstack.org/api-ref/block-storage/v2/#create-snapshot-metadata
- """
- put_body = json.dumps({'metadata': metadata})
- url = "snapshots/%s/metadata" % snapshot_id
- resp, body = self.post(url, put_body)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def show_snapshot_metadata(self, snapshot_id):
- """Get metadata of the snapshot.
-
- For a full list of available parameters, please refer to the official
- API reference:
- http://developer.openstack.org/api-ref/block-storage/v2/#show-snapshot-metadata
- """
- url = "snapshots/%s/metadata" % snapshot_id
- resp, body = self.get(url)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def update_snapshot_metadata(self, snapshot_id, **kwargs):
- """Update metadata for the snapshot.
-
- For a full list of available parameters, please refer to the official
- API reference:
- http://developer.openstack.org/api-ref/block-storage/v2/#update-snapshot-metadata
- """
- put_body = json.dumps(kwargs)
- url = "snapshots/%s/metadata" % snapshot_id
- resp, body = self.put(url, put_body)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def show_snapshot_metadata_item(self, snapshot_id, id):
- """Show metadata item for the snapshot."""
- url = "snapshots/%s/metadata/%s" % (snapshot_id, id)
- resp, body = self.get(url)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def update_snapshot_metadata_item(self, snapshot_id, id, **kwargs):
- """Update metadata item for the snapshot."""
- # TODO(piyush): Current api-site doesn't contain this API description.
- # After fixing the api-site, we need to fix here also for putting the
- # link to api-site.
- # LP: https://bugs.launchpad.net/openstack-api-site/+bug/1529064
- put_body = json.dumps(kwargs)
- url = "snapshots/%s/metadata/%s" % (snapshot_id, id)
- resp, body = self.put(url, put_body)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def delete_snapshot_metadata_item(self, snapshot_id, id):
- """Delete metadata item for the snapshot."""
- url = "snapshots/%s/metadata/%s" % (snapshot_id, id)
- resp, body = self.delete(url)
- self.expected_success(200, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def force_delete_snapshot(self, snapshot_id):
- """Force Delete Snapshot."""
- post_body = json.dumps({'os-force_delete': {}})
- resp, body = self.post('snapshots/%s/action' % snapshot_id, post_body)
- self.expected_success(202, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def unmanage_snapshot(self, snapshot_id):
- """Unmanage a snapshot."""
- post_body = json.dumps({'os-unmanage': {}})
- url = 'snapshots/%s/action' % (snapshot_id)
- resp, body = self.post(url, post_body)
- self.expected_success(202, resp.status)
- return rest_client.ResponseBody(resp, body)
+SnapshotsClient = moves.moved_class(
+ snapshots_client.SnapshotsClient, 'SnapshotsClient',
+ __name__, version="Rocky", removal_version='?')
diff --git a/tempest/lib/services/volume/v2/transfers_client.py b/tempest/lib/services/volume/v2/transfers_client.py
index 2dfbe7b..701d0ae 100644
--- a/tempest/lib/services/volume/v2/transfers_client.py
+++ b/tempest/lib/services/volume/v2/transfers_client.py
@@ -12,72 +12,11 @@
# 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 debtcollector import moves
-from oslo_serialization import jsonutils as json
-from six.moves.urllib import parse as urllib
-
-from tempest.lib.common import rest_client
+from tempest.lib.services.volume.v3 import transfers_client
-class TransfersClient(rest_client.RestClient):
- """Client class to send CRUD Volume Transfer V2 API requests"""
- api_version = "v2"
-
- def create_volume_transfer(self, **kwargs):
- """Create a volume transfer.
-
- For a full list of available parameters, please refer to the official
- API reference:
- https://developer.openstack.org/api-ref/block-storage/v2/#create-volume-transfer
- """
- post_body = json.dumps({'transfer': kwargs})
- resp, body = self.post('os-volume-transfer', post_body)
- body = json.loads(body)
- self.expected_success(202, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def show_volume_transfer(self, transfer_id):
- """Returns the details of a volume transfer."""
- url = "os-volume-transfer/%s" % transfer_id
- resp, body = self.get(url)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def list_volume_transfers(self, detail=False, **params):
- """List all the volume transfers created.
-
- For a full list of available parameters, please refer to the official
- API reference:
- https://developer.openstack.org/api-ref/block-storage/v2/#list-volume-transfers
- https://developer.openstack.org/api-ref/block-storage/v2/#list-volume-transfers-with-details
- """
- url = 'os-volume-transfer'
- if detail:
- url += '/detail'
- if params:
- url += '?%s' % urllib.urlencode(params)
- resp, body = self.get(url)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def delete_volume_transfer(self, transfer_id):
- """Delete a volume transfer."""
- resp, body = self.delete("os-volume-transfer/%s" % transfer_id)
- self.expected_success(202, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def accept_volume_transfer(self, transfer_id, **kwargs):
- """Accept a volume transfer.
-
- For a full list of available parameters, please refer to the official
- API reference:
- https://developer.openstack.org/api-ref/block-storage/v2/#accept-volume-transfer
- """
- url = 'os-volume-transfer/%s/accept' % transfer_id
- post_body = json.dumps({'accept': kwargs})
- resp, body = self.post(url, post_body)
- body = json.loads(body)
- self.expected_success(202, resp.status)
- return rest_client.ResponseBody(resp, body)
+TransfersClient = moves.moved_class(
+ transfers_client.TransfersClient, 'TransfersClient',
+ __name__, version="Rocky", removal_version='?')
diff --git a/tempest/lib/services/volume/v2/types_client.py b/tempest/lib/services/volume/v2/types_client.py
index af4fd8c..8457f91 100644
--- a/tempest/lib/services/volume/v2/types_client.py
+++ b/tempest/lib/services/volume/v2/types_client.py
@@ -12,194 +12,11 @@
# 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 debtcollector import moves
-from oslo_serialization import jsonutils as json
-from six.moves.urllib import parse as urllib
-
-from tempest.lib.common import rest_client
-from tempest.lib import exceptions as lib_exc
+from tempest.lib.services.volume.v3 import types_client
-class TypesClient(rest_client.RestClient):
- """Client class to send CRUD Volume V2 API requests"""
- api_version = "v2"
-
- def is_resource_deleted(self, id):
- try:
- self.show_volume_type(id)
- except lib_exc.NotFound:
- return True
- return False
-
- @property
- def resource_type(self):
- """Returns the primary type of resource this client works with."""
- return 'volume-type'
-
- def list_volume_types(self, **params):
- """List all the volume_types created.
-
- For a full list of available parameters, please refer to the official
- API reference:
- https://developer.openstack.org/api-ref/block-storage/v2/#list-all-volume-types-for-v2
- """
- url = 'types'
- if params:
- url += '?%s' % urllib.urlencode(params)
-
- resp, body = self.get(url)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def show_volume_type(self, volume_type_id):
- """Returns the details of a single volume_type.
-
- For a full list of available parameters, please refer to the official
- API reference:
- https://developer.openstack.org/api-ref/block-storage/v2/#show-volume-type-details-for-v2
- """
- url = "types/%s" % volume_type_id
- resp, body = self.get(url)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def create_volume_type(self, **kwargs):
- """Create volume type.
-
- For a full list of available parameters, please refer to the official
- API reference:
- https://developer.openstack.org/api-ref/block-storage/v2/#create-volume-type-for-v2
- """
- post_body = json.dumps({'volume_type': kwargs})
- resp, body = self.post('types', post_body)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def delete_volume_type(self, volume_type_id):
- """Deletes the Specified Volume_type.
-
- For a full list of available parameters, please refer to the official
- API reference:
- http://developer.openstack.org/api-ref/block-storage/v2/#delete-volume-type
- """
- resp, body = self.delete("types/%s" % volume_type_id)
- self.expected_success(202, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def list_volume_types_extra_specs(self, volume_type_id, **params):
- """List all the volume_types extra specs created.
-
- TODO: Current api-site doesn't contain this API description.
- After fixing the api-site, we need to fix here also for putting
- the link to api-site.
- """
- url = 'types/%s/extra_specs' % volume_type_id
- if params:
- url += '?%s' % urllib.urlencode(params)
-
- resp, body = self.get(url)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def show_volume_type_extra_specs(self, volume_type_id, extra_specs_name):
- """Returns the details of a single volume_type extra spec."""
- url = "types/%s/extra_specs/%s" % (volume_type_id, extra_specs_name)
- resp, body = self.get(url)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def create_volume_type_extra_specs(self, volume_type_id, extra_specs):
- """Creates a new Volume_type extra spec.
-
- volume_type_id: Id of volume_type.
- extra_specs: A dictionary of values to be used as extra_specs.
- """
- url = "types/%s/extra_specs" % volume_type_id
- post_body = json.dumps({'extra_specs': extra_specs})
- resp, body = self.post(url, post_body)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def delete_volume_type_extra_specs(self, volume_type_id, extra_spec_name):
- """Deletes the Specified Volume_type extra spec."""
- resp, body = self.delete("types/%s/extra_specs/%s" % (
- volume_type_id, extra_spec_name))
- self.expected_success(202, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def update_volume_type(self, volume_type_id, **kwargs):
- """Updates volume type name, description, and/or is_public.
-
- For a full list of available parameters, please refer to the official
- API reference:
- http://developer.openstack.org/api-ref/block-storage/v2/#update-volume-type
- """
- put_body = json.dumps({'volume_type': kwargs})
- resp, body = self.put('types/%s' % volume_type_id, put_body)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def update_volume_type_extra_specs(self, volume_type_id, extra_spec_name,
- extra_specs):
- """Update a volume_type extra spec.
-
- volume_type_id: Id of volume_type.
- extra_spec_name: Name of the extra spec to be updated.
- extra_spec: A dictionary of with key as extra_spec_name and the
- updated value.
- For a full list of available parameters, please refer to the official
- API reference:
- http://developer.openstack.org/api-ref/block-storage/v2/#update-extra-specs-for-a-volume-type
- """
- url = "types/%s/extra_specs/%s" % (volume_type_id, extra_spec_name)
- put_body = json.dumps(extra_specs)
- resp, body = self.put(url, put_body)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def add_type_access(self, volume_type_id, **kwargs):
- """Adds volume type access for the given project.
-
- For a full list of available parameters, please refer to the official
- API reference:
- http://developer.openstack.org/api-ref/block-storage/v2/#add-private-volume-type-access
- """
- post_body = json.dumps({'addProjectAccess': kwargs})
- url = 'types/%s/action' % volume_type_id
- resp, body = self.post(url, post_body)
- self.expected_success(202, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def remove_type_access(self, volume_type_id, **kwargs):
- """Removes volume type access for the given project.
-
- For a full list of available parameters, please refer to the official
- API reference:
- http://developer.openstack.org/api-ref/block-storage/v2/#remove-private-volume-type-access
- """
- post_body = json.dumps({'removeProjectAccess': kwargs})
- url = 'types/%s/action' % volume_type_id
- resp, body = self.post(url, post_body)
- self.expected_success(202, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def list_type_access(self, volume_type_id):
- """Print access information about the given volume type.
-
- For a full list of available parameters, please refer to the official
- API reference:
- http://developer.openstack.org/api-ref/block-storage/v2/#list-private-volume-type-access-details
- """
- url = 'types/%s/os-volume-type-access' % volume_type_id
- resp, body = self.get(url)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return rest_client.ResponseBody(resp, body)
+TypesClient = moves.moved_class(
+ types_client.TypesClient, 'TypesClient',
+ __name__, version="Rocky", removal_version='?')
diff --git a/tempest/lib/services/volume/v2/volume_manage_client.py b/tempest/lib/services/volume/v2/volume_manage_client.py
index 12f4240..0669326 100644
--- a/tempest/lib/services/volume/v2/volume_manage_client.py
+++ b/tempest/lib/services/volume/v2/volume_manage_client.py
@@ -12,26 +12,11 @@
# 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 debtcollector import moves
-from oslo_serialization import jsonutils as json
-
-from tempest.lib.common import rest_client
+from tempest.lib.services.volume.v3 import volume_manage_client
-class VolumeManageClient(rest_client.RestClient):
- """Volume manage V2 client."""
-
- api_version = "v2"
-
- def manage_volume(self, **kwargs):
- """Manage existing volume.
-
- For a full list of available parameters, please refer to the official
- API reference:
- https://developer.openstack.org/api-ref/block-storage/v2/#manage-existing-volume
- """
- post_body = json.dumps({'volume': kwargs})
- resp, body = self.post('os-volume-manage', post_body)
- self.expected_success(202, resp.status)
- body = json.loads(body)
- return rest_client.ResponseBody(resp, body)
+VolumeManageClient = moves.moved_class(
+ volume_manage_client.VolumeManageClient, 'VolumeManageClient',
+ __name__, version="Rocky", removal_version='?')
diff --git a/tempest/lib/services/volume/v2/volumes_client.py b/tempest/lib/services/volume/v2/volumes_client.py
index da3f2b5..f5f9e6e 100644
--- a/tempest/lib/services/volume/v2/volumes_client.py
+++ b/tempest/lib/services/volume/v2/volumes_client.py
@@ -12,341 +12,11 @@
# 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 debtcollector import moves
-from oslo_serialization import jsonutils as json
-import six
-from six.moves.urllib import parse as urllib
-
-from tempest.lib.common import rest_client
-from tempest.lib import exceptions as lib_exc
-from tempest.lib.services.volume import base_client
+from tempest.lib.services.volume.v3 import volumes_client
-class VolumesClient(base_client.BaseClient):
- """Client class to send CRUD Volume V2 API requests"""
- api_version = "v2"
-
- def _prepare_params(self, params):
- """Prepares params for use in get or _ext_get methods.
-
- If params is a string it will be left as it is, but if it's not it will
- be urlencoded.
- """
- if isinstance(params, six.string_types):
- return params
- return urllib.urlencode(params)
-
- def list_volumes(self, detail=False, params=None):
- """List all the volumes created.
-
- Params can be a string (must be urlencoded) or a dictionary.
- For a full list of available parameters, please refer to the official
- API reference:
- http://developer.openstack.org/api-ref/block-storage/v2/#list-volumes-with-details
- http://developer.openstack.org/api-ref/block-storage/v2/#list-volumes
- """
- url = 'volumes'
- if detail:
- url += '/detail'
- if params:
- url += '?%s' % self._prepare_params(params)
-
- resp, body = self.get(url)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def show_volume(self, volume_id):
- """Returns the details of a single volume."""
- url = "volumes/%s" % volume_id
- resp, body = self.get(url)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def create_volume(self, **kwargs):
- """Creates a new Volume.
-
- For a full list of available parameters, please refer to the official
- API reference:
- http://developer.openstack.org/api-ref/block-storage/v2/#create-volume
- """
- post_body = json.dumps({'volume': kwargs})
- resp, body = self.post('volumes', post_body)
- body = json.loads(body)
- self.expected_success(202, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def update_volume(self, volume_id, **kwargs):
- """Updates the Specified Volume.
-
- For a full list of available parameters, please refer to the official
- API reference:
- http://developer.openstack.org/api-ref/block-storage/v2/#update-volume
- """
- put_body = json.dumps({'volume': kwargs})
- resp, body = self.put('volumes/%s' % volume_id, put_body)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def delete_volume(self, volume_id, **params):
- """Deletes the Specified Volume.
-
- For a full list of available parameters, please refer to the official
- API reference:
- https://developer.openstack.org/api-ref/block-storage/v2/#delete-volume
- """
- url = 'volumes/%s' % volume_id
- if params:
- url += '?%s' % urllib.urlencode(params)
- resp, body = self.delete(url)
- self.expected_success(202, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def upload_volume(self, volume_id, **kwargs):
- """Uploads a volume in Glance."""
- post_body = json.dumps({'os-volume_upload_image': kwargs})
- url = 'volumes/%s/action' % (volume_id)
- resp, body = self.post(url, post_body)
- body = json.loads(body)
- self.expected_success(202, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def attach_volume(self, volume_id, **kwargs):
- """Attaches a volume to a given instance on a given mountpoint.
-
- For a full list of available parameters, please refer to the official
- API reference:
- http://developer.openstack.org/api-ref/block-storage/v2/#attach-volume-to-server
- """
- post_body = json.dumps({'os-attach': kwargs})
- url = 'volumes/%s/action' % (volume_id)
- resp, body = self.post(url, post_body)
- self.expected_success(202, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def set_bootable_volume(self, volume_id, **kwargs):
- """Set a bootable flag for a volume - true or false.
-
- For a full list of available parameters, please refer to the official
- API reference:
- http://developer.openstack.org/api-ref/block-storage/v2/#update-volume-bootable-status
- """
- post_body = json.dumps({'os-set_bootable': kwargs})
- url = 'volumes/%s/action' % (volume_id)
- resp, body = self.post(url, post_body)
- self.expected_success(200, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def detach_volume(self, volume_id):
- """Detaches a volume from an instance."""
- post_body = json.dumps({'os-detach': {}})
- url = 'volumes/%s/action' % (volume_id)
- resp, body = self.post(url, post_body)
- self.expected_success(202, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def reserve_volume(self, volume_id):
- """Reserves a volume."""
- post_body = json.dumps({'os-reserve': {}})
- url = 'volumes/%s/action' % (volume_id)
- resp, body = self.post(url, post_body)
- self.expected_success(202, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def unreserve_volume(self, volume_id):
- """Restore a reserved volume ."""
- post_body = json.dumps({'os-unreserve': {}})
- url = 'volumes/%s/action' % (volume_id)
- resp, body = self.post(url, post_body)
- self.expected_success(202, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def is_resource_deleted(self, id):
- """Check the specified resource is deleted or not.
-
- :param id: A checked resource id
- :raises lib_exc.DeleteErrorException: If the specified resource is on
- the status the delete was failed.
- """
- try:
- volume = self.show_volume(id)
- except lib_exc.NotFound:
- return True
- if volume["volume"]["status"] == "error_deleting":
- raise lib_exc.DeleteErrorException(resource_id=id)
- return False
-
- @property
- def resource_type(self):
- """Returns the primary type of resource this client works with."""
- return 'volume'
-
- def extend_volume(self, volume_id, **kwargs):
- """Extend a volume.
-
- For a full list of available parameters, please refer to the official
- API reference:
- http://developer.openstack.org/api-ref/block-storage/v2/#extend-volume-size
- """
- post_body = json.dumps({'os-extend': kwargs})
- url = 'volumes/%s/action' % (volume_id)
- resp, body = self.post(url, post_body)
- self.expected_success(202, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def reset_volume_status(self, volume_id, **kwargs):
- """Reset the Specified Volume's Status.
-
- For a full list of available parameters, please refer to the official
- API reference:
- http://developer.openstack.org/api-ref/block-storage/v2/#reset-volume-statuses
- """
- post_body = json.dumps({'os-reset_status': kwargs})
- resp, body = self.post('volumes/%s/action' % volume_id, post_body)
- self.expected_success(202, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def update_volume_readonly(self, volume_id, **kwargs):
- """Update the Specified Volume readonly."""
- post_body = json.dumps({'os-update_readonly_flag': kwargs})
- url = 'volumes/%s/action' % (volume_id)
- resp, body = self.post(url, post_body)
- self.expected_success(202, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def force_delete_volume(self, volume_id):
- """Force Delete Volume."""
- post_body = json.dumps({'os-force_delete': {}})
- resp, body = self.post('volumes/%s/action' % volume_id, post_body)
- self.expected_success(202, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def create_volume_metadata(self, volume_id, metadata):
- """Create metadata for the volume.
-
- For a full list of available parameters, please refer to the official
- API reference:
- http://developer.openstack.org/api-ref/block-storage/v2/#create-volume-metadata
- """
- put_body = json.dumps({'metadata': metadata})
- url = "volumes/%s/metadata" % volume_id
- resp, body = self.post(url, put_body)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def show_volume_metadata(self, volume_id):
- """Get metadata of the volume."""
- url = "volumes/%s/metadata" % volume_id
- resp, body = self.get(url)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def update_volume_metadata(self, volume_id, metadata):
- """Update metadata for the volume.
-
- For a full list of available parameters, please refer to the official
- API reference:
- http://developer.openstack.org/api-ref/block-storage/v2/#update-volume-metadata
- """
- put_body = json.dumps({'metadata': metadata})
- url = "volumes/%s/metadata" % volume_id
- resp, body = self.put(url, put_body)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def show_volume_metadata_item(self, volume_id, id):
- """Show metadata item for the volume."""
- url = "volumes/%s/metadata/%s" % (volume_id, id)
- resp, body = self.get(url)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def update_volume_metadata_item(self, volume_id, id, meta_item):
- """Update metadata item for the volume."""
- put_body = json.dumps({'meta': meta_item})
- url = "volumes/%s/metadata/%s" % (volume_id, id)
- resp, body = self.put(url, put_body)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def delete_volume_metadata_item(self, volume_id, id):
- """Delete metadata item for the volume."""
- url = "volumes/%s/metadata/%s" % (volume_id, id)
- resp, body = self.delete(url)
- self.expected_success(200, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def retype_volume(self, volume_id, **kwargs):
- """Updates volume with new volume type.
-
- For a full list of available parameters, please refer to the official
- API reference:
- https://developer.openstack.org/api-ref/block-storage/v2/#retype-volume
- """
- post_body = json.dumps({'os-retype': kwargs})
- resp, body = self.post('volumes/%s/action' % volume_id, post_body)
- self.expected_success(202, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def force_detach_volume(self, volume_id, **kwargs):
- """Force detach a volume.
-
- For a full list of available parameters, please refer to the official
- API reference:
- https://developer.openstack.org/api-ref/block-storage/v2/#force-detach-volume
- """
- post_body = json.dumps({'os-force_detach': kwargs})
- url = 'volumes/%s/action' % volume_id
- resp, body = self.post(url, post_body)
- self.expected_success(202, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def update_volume_image_metadata(self, volume_id, **kwargs):
- """Update image metadata for the volume.
-
- For a full list of available parameters, please refer to the official
- API reference:
- http://developer.openstack.org/api-ref/block-storage/v2/#set-image-metadata-for-volume
- """
- post_body = json.dumps({'os-set_image_metadata': {'metadata': kwargs}})
- url = "volumes/%s/action" % (volume_id)
- resp, body = self.post(url, post_body)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def delete_volume_image_metadata(self, volume_id, key_name):
- """Delete image metadata item for the volume."""
- post_body = json.dumps({'os-unset_image_metadata': {'key': key_name}})
- url = "volumes/%s/action" % (volume_id)
- resp, body = self.post(url, post_body)
- self.expected_success(200, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def show_volume_image_metadata(self, volume_id):
- """Show image metadata for the volume."""
- post_body = json.dumps({'os-show_image_metadata': {}})
- url = "volumes/%s/action" % volume_id
- resp, body = self.post(url, post_body)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- def unmanage_volume(self, volume_id):
- """Unmanage volume.
-
- For a full list of available parameters, please refer to the official
- API reference:
- https://developer.openstack.org/api-ref/block-storage/v2/#unmanage-volume
- """
- post_body = json.dumps({'os-unmanage': {}})
- resp, body = self.post('volumes/%s/action' % volume_id, post_body)
- self.expected_success(202, resp.status)
- return rest_client.ResponseBody(resp, body)
+VolumesClient = moves.moved_class(
+ volumes_client.VolumesClient, 'VolumesClient',
+ __name__, version="Rocky", removal_version='?')
diff --git a/tempest/lib/services/volume/v3/__init__.py b/tempest/lib/services/volume/v3/__init__.py
index 2d85553..a1b7de3 100644
--- a/tempest/lib/services/volume/v3/__init__.py
+++ b/tempest/lib/services/volume/v3/__init__.py
@@ -11,19 +11,44 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
-
+from tempest.lib.services.volume.v3.availability_zone_client \
+ import AvailabilityZoneClient
from tempest.lib.services.volume.v3.backups_client import BackupsClient
from tempest.lib.services.volume.v3.base_client import BaseClient
+from tempest.lib.services.volume.v3.capabilities_client import \
+ CapabilitiesClient
+from tempest.lib.services.volume.v3.encryption_types_client import \
+ EncryptionTypesClient
+from tempest.lib.services.volume.v3.extensions_client import ExtensionsClient
from tempest.lib.services.volume.v3.group_snapshots_client import \
GroupSnapshotsClient
from tempest.lib.services.volume.v3.group_types_client import GroupTypesClient
from tempest.lib.services.volume.v3.groups_client import GroupsClient
+from tempest.lib.services.volume.v3.hosts_client import HostsClient
+from tempest.lib.services.volume.v3.limits_client import LimitsClient
from tempest.lib.services.volume.v3.messages_client import MessagesClient
+from tempest.lib.services.volume.v3.qos_client import QosSpecsClient
+from tempest.lib.services.volume.v3.quota_classes_client import \
+ QuotaClassesClient
+from tempest.lib.services.volume.v3.quotas_client import QuotasClient
+from tempest.lib.services.volume.v3.scheduler_stats_client import \
+ SchedulerStatsClient
+from tempest.lib.services.volume.v3.services_client import ServicesClient
+from tempest.lib.services.volume.v3.snapshot_manage_client import \
+ SnapshotManageClient
from tempest.lib.services.volume.v3.snapshots_client import SnapshotsClient
+from tempest.lib.services.volume.v3.transfers_client import TransfersClient
+from tempest.lib.services.volume.v3.types_client import TypesClient
from tempest.lib.services.volume.v3.versions_client import VersionsClient
+from tempest.lib.services.volume.v3.volume_manage_client import \
+ VolumeManageClient
from tempest.lib.services.volume.v3.volumes_client import VolumesClient
-__all__ = ['BackupsClient', 'BaseClient', 'GroupsClient',
- 'GroupSnapshotsClient', 'GroupTypesClient',
- 'MessagesClient', 'SnapshotsClient', 'VersionsClient',
- 'VolumesClient']
+__all__ = ['AvailabilityZoneClient', 'BackupsClient', 'BaseClient',
+ 'CapabilitiesClient', 'EncryptionTypesClient', 'ExtensionsClient',
+ 'GroupSnapshotsClient', 'GroupTypesClient', 'GroupsClient',
+ 'HostsClient', 'LimitsClient', 'MessagesClient', 'QosSpecsClient',
+ 'QuotaClassesClient', 'QuotasClient', 'SchedulerStatsClient',
+ 'ServicesClient', 'SnapshotManageClient', 'SnapshotsClient',
+ 'TransfersClient', 'TypesClient', 'VersionsClient',
+ 'VolumeManageClient', 'VolumesClient']
diff --git a/tempest/lib/services/volume/v3/availability_zone_client.py b/tempest/lib/services/volume/v3/availability_zone_client.py
new file mode 100644
index 0000000..147e4c6
--- /dev/null
+++ b/tempest/lib/services/volume/v3/availability_zone_client.py
@@ -0,0 +1,27 @@
+# Copyright 2014 IBM Corp.
+# 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 oslo_serialization import jsonutils as json
+
+from tempest.lib.common import rest_client
+
+
+class AvailabilityZoneClient(rest_client.RestClient):
+
+ def list_availability_zones(self):
+ resp, body = self.get('os-availability-zone')
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/volume/v3/backups_client.py b/tempest/lib/services/volume/v3/backups_client.py
index e742e39..10538b0 100644
--- a/tempest/lib/services/volume/v3/backups_client.py
+++ b/tempest/lib/services/volume/v3/backups_client.py
@@ -14,15 +14,30 @@
# under the License.
from oslo_serialization import jsonutils as json
+from six.moves.urllib import parse as urllib
from tempest.lib.common import rest_client
-from tempest.lib.services.volume.v2 import backups_client
+from tempest.lib import exceptions as lib_exc
+from tempest.lib.services.volume import base_client
-class BackupsClient(backups_client.BackupsClient):
+class BackupsClient(base_client.BaseClient):
"""Volume V3 Backups client"""
api_version = "v3"
+ def create_backup(self, **kwargs):
+ """Creates a backup of volume.
+
+ 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#create-a-backup
+ """
+ post_body = json.dumps({'backup': kwargs})
+ resp, body = self.post('backups', post_body)
+ body = json.loads(body)
+ self.expected_success(202, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
def update_backup(self, backup_id, **kwargs):
"""Updates the specified volume backup.
@@ -35,3 +50,83 @@
body = json.loads(body)
self.expected_success(200, resp.status)
return rest_client.ResponseBody(resp, body)
+
+ def restore_backup(self, backup_id, **kwargs):
+ """Restore volume from backup.
+
+ 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#restore-a-backup
+ """
+ post_body = json.dumps({'restore': kwargs})
+ resp, body = self.post('backups/%s/restore' % (backup_id), post_body)
+ body = json.loads(body)
+ self.expected_success(202, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_backup(self, backup_id):
+ """Delete a backup of volume."""
+ resp, body = self.delete('backups/%s' % backup_id)
+ self.expected_success(202, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_backup(self, backup_id):
+ """Returns the details of a single backup."""
+ url = "backups/%s" % backup_id
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_backups(self, detail=False, **params):
+ """List all the tenant's backups.
+
+ 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#list-backups-for-project
+ https://developer.openstack.org/api-ref/block-storage/v3/index.html#list-backups-with-detail
+ """
+ url = "backups"
+ if detail:
+ url += "/detail"
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def export_backup(self, backup_id):
+ """Export backup metadata record."""
+ url = "backups/%s/export_record" % backup_id
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def import_backup(self, **kwargs):
+ """Import backup metadata record."""
+ post_body = json.dumps({'backup-record': kwargs})
+ resp, body = self.post("backups/import_record", post_body)
+ body = json.loads(body)
+ self.expected_success(201, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def reset_backup_status(self, backup_id, status):
+ """Reset the specified backup's status."""
+ post_body = json.dumps({'os-reset_status': {"status": status}})
+ resp, body = self.post('backups/%s/action' % backup_id, post_body)
+ self.expected_success(202, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def is_resource_deleted(self, id):
+ try:
+ self.show_backup(id)
+ except lib_exc.NotFound:
+ return True
+ return False
+
+ @property
+ def resource_type(self):
+ """Returns the primary type of resource this client works with."""
+ return 'backup'
diff --git a/tempest/lib/services/volume/v3/capabilities_client.py b/tempest/lib/services/volume/v3/capabilities_client.py
new file mode 100644
index 0000000..7ebcd69
--- /dev/null
+++ b/tempest/lib/services/volume/v3/capabilities_client.py
@@ -0,0 +1,34 @@
+# Copyright 2016 Red Hat, Inc.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from oslo_serialization import jsonutils as json
+
+from tempest.lib.common import rest_client
+
+
+class CapabilitiesClient(rest_client.RestClient):
+
+ def show_backend_capabilities(self, host):
+ """Shows capabilities for a storage back end.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/block-storage/v2/index.html#show-back-end-capabilities
+ """
+ url = 'capabilities/%s' % host
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/volume/v3/encryption_types_client.py b/tempest/lib/services/volume/v3/encryption_types_client.py
new file mode 100644
index 0000000..7443a87
--- /dev/null
+++ b/tempest/lib/services/volume/v3/encryption_types_client.py
@@ -0,0 +1,90 @@
+# Copyright 2012 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 oslo_serialization import jsonutils as json
+
+from tempest.lib.common import rest_client
+from tempest.lib import exceptions as lib_exc
+
+
+class EncryptionTypesClient(rest_client.RestClient):
+
+ def is_resource_deleted(self, id):
+ try:
+ body = self.show_encryption_type(id)
+ if not body:
+ return True
+ except lib_exc.NotFound:
+ return True
+ return False
+
+ @property
+ def resource_type(self):
+ """Returns the primary type of resource this client works with."""
+ return 'encryption-type'
+
+ def show_encryption_type(self, volume_type_id):
+ """Get the volume encryption type for the specified volume type.
+
+ volume_type_id: Id of volume_type.
+ """
+ url = "/types/%s/encryption" % volume_type_id
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_encryption_specs_item(self, volume_type_id, key):
+ """Get the encryption specs item for the specified volume type."""
+ url = "/types/%s/encryption/%s" % (volume_type_id, key)
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def create_encryption_type(self, volume_type_id, **kwargs):
+ """Create encryption type.
+
+ 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#create-an-encryption-type
+ """
+ url = "/types/%s/encryption" % volume_type_id
+ post_body = json.dumps({'encryption': kwargs})
+ resp, body = self.post(url, post_body)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_encryption_type(self, volume_type_id):
+ """Delete the encryption type for the specified volume-type."""
+ resp, body = self.delete(
+ "/types/%s/encryption/provider" % volume_type_id)
+ self.expected_success(202, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_encryption_type(self, volume_type_id, **kwargs):
+ """Update an encryption type for an existing volume type.
+
+ 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#update-an-encryption-type
+ """
+ url = "/types/%s/encryption/provider" % volume_type_id
+ put_body = json.dumps({'encryption': kwargs})
+ resp, body = self.put(url, put_body)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/volume/v3/extensions_client.py b/tempest/lib/services/volume/v3/extensions_client.py
new file mode 100644
index 0000000..45b7a56
--- /dev/null
+++ b/tempest/lib/services/volume/v3/extensions_client.py
@@ -0,0 +1,29 @@
+# Copyright 2014 IBM Corp.
+# 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 oslo_serialization import jsonutils as json
+
+from tempest.lib.common import rest_client
+
+
+class ExtensionsClient(rest_client.RestClient):
+ """Volume extensions client."""
+
+ def list_extensions(self):
+ url = 'extensions'
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/volume/v3/hosts_client.py b/tempest/lib/services/volume/v3/hosts_client.py
new file mode 100644
index 0000000..8b65805
--- /dev/null
+++ b/tempest/lib/services/volume/v3/hosts_client.py
@@ -0,0 +1,47 @@
+# Copyright 2014 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 oslo_serialization import jsonutils as json
+from six.moves.urllib import parse as urllib
+
+from tempest.lib.common import rest_client
+
+
+class HostsClient(rest_client.RestClient):
+ """Client class to send CRUD Volume API requests"""
+
+ def list_hosts(self, **params):
+ """Lists all hosts.
+
+ 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#list-all-hosts-for-a-project
+ """
+ url = 'os-hosts'
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_host(self, host_name):
+ """Show host details."""
+ url = 'os-hosts/%s' % host_name
+ resp, body = self.get(url)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/volume/v3/limits_client.py b/tempest/lib/services/volume/v3/limits_client.py
new file mode 100644
index 0000000..9500254
--- /dev/null
+++ b/tempest/lib/services/volume/v3/limits_client.py
@@ -0,0 +1,30 @@
+# Copyright 2016 Red Hat, Inc.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from oslo_serialization import jsonutils as json
+
+from tempest.lib.common import rest_client
+
+
+class LimitsClient(rest_client.RestClient):
+ """Volume limits client."""
+
+ def show_limits(self):
+ """Returns the details of a volume absolute limits."""
+ url = "limits"
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/volume/v3/qos_client.py b/tempest/lib/services/volume/v3/qos_client.py
new file mode 100644
index 0000000..8f4d37f
--- /dev/null
+++ b/tempest/lib/services/volume/v3/qos_client.py
@@ -0,0 +1,131 @@
+# 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 oslo_serialization import jsonutils as json
+
+from tempest.lib.common import rest_client
+from tempest.lib import exceptions as lib_exc
+
+
+class QosSpecsClient(rest_client.RestClient):
+ """Volume QoS client.
+
+ Client class to send CRUD QoS API requests
+ """
+
+ def is_resource_deleted(self, qos_id):
+ try:
+ self.show_qos(qos_id)
+ except lib_exc.NotFound:
+ return True
+ return False
+
+ @property
+ def resource_type(self):
+ """Returns the primary type of resource this client works with."""
+ return 'qos'
+
+ def create_qos(self, **kwargs):
+ """Create a QoS Specification.
+
+ 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#create-a-qos-specification
+ """
+ post_body = json.dumps({'qos_specs': kwargs})
+ resp, body = self.post('qos-specs', post_body)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_qos(self, qos_id, force=False):
+ """Delete the specified QoS specification."""
+ resp, body = self.delete(
+ "qos-specs/%s?force=%s" % (qos_id, force))
+ self.expected_success(202, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_qos(self):
+ """List all the QoS specifications created."""
+ url = 'qos-specs'
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_qos(self, qos_id):
+ """Get the specified QoS specification."""
+ url = "qos-specs/%s" % qos_id
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def set_qos_key(self, qos_id, **kwargs):
+ """Set the specified keys/values of QoS specification.
+
+ 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#set-keys-in-a-qos-specification
+ """
+ put_body = json.dumps({"qos_specs": kwargs})
+ resp, body = self.put('qos-specs/%s' % qos_id, put_body)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def unset_qos_key(self, qos_id, keys):
+ """Unset the specified keys of QoS specification.
+
+ :param keys: keys to delete from the QoS specification.
+
+ 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#unset-keys-in-a-qos-specification
+ """
+ put_body = json.dumps({'keys': keys})
+ resp, body = self.put('qos-specs/%s/delete_keys' % qos_id, put_body)
+ self.expected_success(202, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def associate_qos(self, qos_id, vol_type_id):
+ """Associate the specified QoS with specified volume-type."""
+ url = "qos-specs/%s/associate" % qos_id
+ url += "?vol_type_id=%s" % vol_type_id
+ resp, body = self.get(url)
+ self.expected_success(202, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_association_qos(self, qos_id):
+ """Get the association of the specified QoS specification."""
+ url = "qos-specs/%s/associations" % qos_id
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def disassociate_qos(self, qos_id, vol_type_id):
+ """Disassociate the specified QoS with specified volume-type."""
+ url = "qos-specs/%s/disassociate" % qos_id
+ url += "?vol_type_id=%s" % vol_type_id
+ resp, body = self.get(url)
+ self.expected_success(202, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def disassociate_all_qos(self, qos_id):
+ """Disassociate the specified QoS with all associations."""
+ url = "qos-specs/%s/disassociate_all" % qos_id
+ resp, body = self.get(url)
+ self.expected_success(202, resp.status)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/volume/v3/quota_classes_client.py b/tempest/lib/services/volume/v3/quota_classes_client.py
new file mode 100644
index 0000000..a8eb536
--- /dev/null
+++ b/tempest/lib/services/volume/v3/quota_classes_client.py
@@ -0,0 +1,49 @@
+# Copyright 2017 FiberHome Telecommunication Technologies CO.,LTD
+# 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 oslo_serialization import jsonutils as json
+
+from tempest.lib.common import rest_client
+
+
+class QuotaClassesClient(rest_client.RestClient):
+ """Volume quota class client."""
+
+ def show_quota_class_set(self, quota_class_id):
+ """List quotas for a quota class.
+
+ 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#show-quota-classes-for-a-project
+ """
+ url = 'os-quota-class-sets/%s' % quota_class_id
+ resp, body = self.get(url)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_quota_class_set(self, quota_class_id, **kwargs):
+ """Update quotas for a quota class.
+
+ 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#update-quota-classes-for-a-project
+ """
+ url = 'os-quota-class-sets/%s' % quota_class_id
+ put_body = json.dumps({'quota_class_set': kwargs})
+ resp, body = self.put(url, put_body)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/volume/v3/quotas_client.py b/tempest/lib/services/volume/v3/quotas_client.py
new file mode 100644
index 0000000..538a915
--- /dev/null
+++ b/tempest/lib/services/volume/v3/quotas_client.py
@@ -0,0 +1,63 @@
+# Copyright 2014 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 oslo_serialization import jsonutils
+from six.moves.urllib import parse as urllib
+
+from tempest.lib.common import rest_client
+
+
+class QuotasClient(rest_client.RestClient):
+ """Client class to send CRUD Volume Quotas API requests"""
+
+ def show_default_quota_set(self, tenant_id):
+ """List the default volume quota set for a tenant."""
+
+ url = 'os-quota-sets/%s/defaults' % tenant_id
+ resp, body = self.get(url)
+ self.expected_success(200, resp.status)
+ body = jsonutils.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_quota_set(self, tenant_id, params=None):
+ """List the quota set for a tenant."""
+
+ url = 'os-quota-sets/%s' % tenant_id
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+
+ resp, body = self.get(url)
+ self.expected_success(200, resp.status)
+ body = jsonutils.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_quota_set(self, tenant_id, **kwargs):
+ """Updates quota set
+
+ 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#update-quotas-for-a-project
+ """
+ put_body = jsonutils.dumps({'quota_set': kwargs})
+ resp, body = self.put('os-quota-sets/%s' % tenant_id, put_body)
+ self.expected_success(200, resp.status)
+ body = jsonutils.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_quota_set(self, tenant_id):
+ """Delete the tenant's quota set."""
+ resp, body = self.delete('os-quota-sets/%s' % tenant_id)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/volume/v3/scheduler_stats_client.py b/tempest/lib/services/volume/v3/scheduler_stats_client.py
new file mode 100644
index 0000000..9b80851
--- /dev/null
+++ b/tempest/lib/services/volume/v3/scheduler_stats_client.py
@@ -0,0 +1,36 @@
+# Copyright 2016 Red Hat, Inc.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from oslo_serialization import jsonutils as json
+
+from tempest.lib.common import rest_client
+
+
+class SchedulerStatsClient(rest_client.RestClient):
+
+ def list_pools(self, detail=False):
+ """List all the volumes pools (hosts).
+
+ 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#list-all-back-end-storage-pools
+ """
+ url = 'scheduler-stats/get_pools'
+ if detail:
+ url += '?detail=True'
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/volume/v3/services_client.py b/tempest/lib/services/volume/v3/services_client.py
new file mode 100644
index 0000000..09036a4
--- /dev/null
+++ b/tempest/lib/services/volume/v3/services_client.py
@@ -0,0 +1,33 @@
+# Copyright 2014 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 oslo_serialization import jsonutils as json
+from six.moves.urllib import parse as urllib
+
+from tempest.lib.common import rest_client
+
+
+class ServicesClient(rest_client.RestClient):
+ """Client class to send CRUD Volume API requests"""
+
+ def list_services(self, **params):
+ url = 'os-services'
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/volume/v3/snapshot_manage_client.py b/tempest/lib/services/volume/v3/snapshot_manage_client.py
new file mode 100644
index 0000000..43fd328
--- /dev/null
+++ b/tempest/lib/services/volume/v3/snapshot_manage_client.py
@@ -0,0 +1,31 @@
+# Copyright 2016 Red Hat, Inc.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from oslo_serialization import jsonutils as json
+
+from tempest.lib.common import rest_client
+
+
+class SnapshotManageClient(rest_client.RestClient):
+ """Snapshot manage client."""
+
+ def manage_snapshot(self, **kwargs):
+ """Manage a snapshot."""
+ post_body = json.dumps({'snapshot': kwargs})
+ url = 'os-snapshot-manage'
+ resp, body = self.post(url, post_body)
+ self.expected_success(202, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/volume/v3/snapshots_client.py b/tempest/lib/services/volume/v3/snapshots_client.py
index 88c094f..298925a 100644
--- a/tempest/lib/services/volume/v3/snapshots_client.py
+++ b/tempest/lib/services/volume/v3/snapshots_client.py
@@ -13,9 +13,199 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest.lib.services.volume.v2 import snapshots_client
+from oslo_serialization import jsonutils as json
+from six.moves.urllib import parse as urllib
+
+from tempest.lib.common import rest_client
+from tempest.lib import exceptions as lib_exc
-class SnapshotsClient(snapshots_client.SnapshotsClient):
+class SnapshotsClient(rest_client.RestClient):
"""Client class to send CRUD Volume Snapshot V3 API requests."""
api_version = "v3"
+ create_resp = 202
+
+ def list_snapshots(self, detail=False, **params):
+ """List all the snapshot.
+
+ 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#list-accessible-snapshots
+ https://developer.openstack.org/api-ref/block-storage/v3/index.html#list-snapshots-and-details
+ """
+ url = 'snapshots'
+ if detail:
+ url += '/detail'
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_snapshot(self, snapshot_id):
+ """Returns the details of a single snapshot.
+
+ 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#show-a-snapshot-s-details
+ """
+ url = "snapshots/%s" % snapshot_id
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def create_snapshot(self, **kwargs):
+ """Creates a new snapshot.
+
+ 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#create-a-snapshot
+ """
+ post_body = json.dumps({'snapshot': kwargs})
+ resp, body = self.post('snapshots', post_body)
+ body = json.loads(body)
+ self.expected_success(self.create_resp, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_snapshot(self, snapshot_id, **kwargs):
+ """Updates a snapshot.
+
+ 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#update-a-snapshot
+ """
+ put_body = json.dumps({'snapshot': kwargs})
+ resp, body = self.put('snapshots/%s' % snapshot_id, put_body)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_snapshot(self, snapshot_id):
+ """Delete Snapshot.
+
+ 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#delete-a-snapshot
+ """
+ resp, body = self.delete("snapshots/%s" % snapshot_id)
+ self.expected_success(202, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def is_resource_deleted(self, id):
+ try:
+ self.show_snapshot(id)
+ except lib_exc.NotFound:
+ return True
+ return False
+
+ @property
+ def resource_type(self):
+ """Returns the primary type of resource this client works with."""
+ return 'volume-snapshot'
+
+ def reset_snapshot_status(self, snapshot_id, status):
+ """Reset the specified snapshot's status."""
+ post_body = json.dumps({'os-reset_status': {"status": status}})
+ resp, body = self.post('snapshots/%s/action' % snapshot_id, post_body)
+ self.expected_success(202, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_snapshot_status(self, snapshot_id, **kwargs):
+ """Update the specified snapshot's status."""
+ # TODO(gmann): api-site doesn't contain doc ref
+ # for this API. After fixing the api-site, we need to
+ # add the link here.
+ # Bug https://bugs.launchpad.net/openstack-api-site/+bug/1532645
+
+ post_body = json.dumps({'os-update_snapshot_status': kwargs})
+ url = 'snapshots/%s/action' % snapshot_id
+ resp, body = self.post(url, post_body)
+ self.expected_success(202, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def create_snapshot_metadata(self, snapshot_id, metadata):
+ """Create metadata for the snapshot.
+
+ 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#create-a-snapshot-s-metadata
+ """
+ put_body = json.dumps({'metadata': metadata})
+ url = "snapshots/%s/metadata" % snapshot_id
+ resp, body = self.post(url, put_body)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_snapshot_metadata(self, snapshot_id):
+ """Get metadata of the snapshot.
+
+ 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#show-a-snapshot-s-metadata
+ """
+ url = "snapshots/%s/metadata" % snapshot_id
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_snapshot_metadata(self, snapshot_id, **kwargs):
+ """Update metadata for the snapshot.
+
+ 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#update-a-snapshot-s-metadata
+ """
+ put_body = json.dumps(kwargs)
+ url = "snapshots/%s/metadata" % snapshot_id
+ resp, body = self.put(url, put_body)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_snapshot_metadata_item(self, snapshot_id, id):
+ """Show metadata item for the snapshot."""
+ url = "snapshots/%s/metadata/%s" % (snapshot_id, id)
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_snapshot_metadata_item(self, snapshot_id, id, **kwargs):
+ """Update metadata item for the snapshot."""
+ # TODO(piyush): Current api-site doesn't contain this API description.
+ # After fixing the api-site, we need to fix here also for putting the
+ # link to api-site.
+ # LP: https://bugs.launchpad.net/openstack-api-site/+bug/1529064
+ put_body = json.dumps(kwargs)
+ url = "snapshots/%s/metadata/%s" % (snapshot_id, id)
+ resp, body = self.put(url, put_body)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_snapshot_metadata_item(self, snapshot_id, id):
+ """Delete metadata item for the snapshot."""
+ url = "snapshots/%s/metadata/%s" % (snapshot_id, id)
+ resp, body = self.delete(url)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def force_delete_snapshot(self, snapshot_id):
+ """Force Delete Snapshot."""
+ post_body = json.dumps({'os-force_delete': {}})
+ resp, body = self.post('snapshots/%s/action' % snapshot_id, post_body)
+ self.expected_success(202, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def unmanage_snapshot(self, snapshot_id):
+ """Unmanage a snapshot."""
+ post_body = json.dumps({'os-unmanage': {}})
+ url = 'snapshots/%s/action' % (snapshot_id)
+ resp, body = self.post(url, post_body)
+ self.expected_success(202, resp.status)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/volume/v3/transfers_client.py b/tempest/lib/services/volume/v3/transfers_client.py
new file mode 100644
index 0000000..97c5597
--- /dev/null
+++ b/tempest/lib/services/volume/v3/transfers_client.py
@@ -0,0 +1,82 @@
+# Copyright 2012 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 oslo_serialization import jsonutils as json
+from six.moves.urllib import parse as urllib
+
+from tempest.lib.common import rest_client
+
+
+class TransfersClient(rest_client.RestClient):
+ """Client class to send CRUD Volume Transfer API requests"""
+
+ def create_volume_transfer(self, **kwargs):
+ """Create a volume transfer.
+
+ 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#create-a-volume-transfer
+ """
+ post_body = json.dumps({'transfer': kwargs})
+ resp, body = self.post('os-volume-transfer', post_body)
+ body = json.loads(body)
+ self.expected_success(202, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_volume_transfer(self, transfer_id):
+ """Returns the details of a volume transfer."""
+ url = "os-volume-transfer/%s" % transfer_id
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_volume_transfers(self, detail=False, **params):
+ """List all the volume transfers created.
+
+ 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#list-volume-transfers-for-a-project
+ https://developer.openstack.org/api-ref/block-storage/v3/index.html#list-volume-transfers-and-details
+ """
+ url = 'os-volume-transfer'
+ if detail:
+ url += '/detail'
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_volume_transfer(self, transfer_id):
+ """Delete a volume transfer."""
+ resp, body = self.delete("os-volume-transfer/%s" % transfer_id)
+ self.expected_success(202, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def accept_volume_transfer(self, transfer_id, **kwargs):
+ """Accept a volume transfer.
+
+ 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#accept-a-volume-transfer
+ """
+ url = 'os-volume-transfer/%s/accept' % transfer_id
+ post_body = json.dumps({'accept': kwargs})
+ resp, body = self.post(url, post_body)
+ body = json.loads(body)
+ self.expected_success(202, resp.status)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/volume/v3/types_client.py b/tempest/lib/services/volume/v3/types_client.py
new file mode 100644
index 0000000..6d9d03a
--- /dev/null
+++ b/tempest/lib/services/volume/v3/types_client.py
@@ -0,0 +1,204 @@
+# Copyright 2012 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 oslo_serialization import jsonutils as json
+from six.moves.urllib import parse as urllib
+
+from tempest.lib.common import rest_client
+from tempest.lib import exceptions as lib_exc
+
+
+class TypesClient(rest_client.RestClient):
+ """Client class to send CRUD Volume API requests"""
+
+ def is_resource_deleted(self, id):
+ try:
+ self.show_volume_type(id)
+ except lib_exc.NotFound:
+ return True
+ return False
+
+ @property
+ def resource_type(self):
+ """Returns the primary type of resource this client works with."""
+ return 'volume-type'
+
+ def list_volume_types(self, **params):
+ """List all the volume_types created.
+
+ 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#list-all-volume-types
+ """
+ url = 'types'
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_volume_type(self, volume_type_id):
+ """Returns the details of a single volume_type.
+
+ 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#show-volume-type-detail
+ """
+ url = "types/%s" % volume_type_id
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def create_volume_type(self, **kwargs):
+ """Create volume type.
+
+ 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#create-a-volume-type
+ """
+ post_body = json.dumps({'volume_type': kwargs})
+ resp, body = self.post('types', post_body)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_volume_type(self, volume_type_id):
+ """Deletes the Specified Volume_type.
+
+ 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#delete-a-volume-type
+ """
+ resp, body = self.delete("types/%s" % volume_type_id)
+ self.expected_success(202, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_volume_types_extra_specs(self, volume_type_id, **params):
+ """List all the volume_types extra specs created.
+
+ TODO: Current api-site doesn't contain this API description.
+ After fixing the api-site, we need to fix here also for putting
+ the link to api-site.
+ """
+ url = 'types/%s/extra_specs' % volume_type_id
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_volume_type_extra_specs(self, volume_type_id, extra_specs_name):
+ """Returns the details of a single volume_type extra spec."""
+ url = "types/%s/extra_specs/%s" % (volume_type_id, extra_specs_name)
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def create_volume_type_extra_specs(self, volume_type_id, extra_specs):
+ """Creates a new Volume_type extra spec.
+
+ volume_type_id: Id of volume_type.
+ extra_specs: A dictionary of values to be used as extra_specs.
+ """
+ url = "types/%s/extra_specs" % volume_type_id
+ post_body = json.dumps({'extra_specs': extra_specs})
+ resp, body = self.post(url, post_body)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_volume_type_extra_specs(self, volume_type_id, extra_spec_name):
+ """Deletes the Specified Volume_type extra spec."""
+ resp, body = self.delete("types/%s/extra_specs/%s" % (
+ volume_type_id, extra_spec_name))
+ self.expected_success(202, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_volume_type(self, volume_type_id, **kwargs):
+ """Updates volume type name, description, and/or is_public.
+
+ 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#update-a-volume-type
+ """
+ put_body = json.dumps({'volume_type': kwargs})
+ resp, body = self.put('types/%s' % volume_type_id, put_body)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_volume_type_extra_specs(self, volume_type_id, extra_spec_name,
+ extra_specs):
+ """Update a volume_type extra spec.
+
+ volume_type_id: Id of volume_type.
+ extra_spec_name: Name of the extra spec to be updated.
+ extra_spec: A dictionary of with key as extra_spec_name and the
+ updated value.
+ 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#update-extra-specification-for-volume-type
+ """
+ url = "types/%s/extra_specs/%s" % (volume_type_id, extra_spec_name)
+ put_body = json.dumps(extra_specs)
+ resp, body = self.put(url, put_body)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def add_type_access(self, volume_type_id, **kwargs):
+ """Adds volume type access for the given project.
+
+ 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#add-private-volume-type-access-to-project
+ """
+ post_body = json.dumps({'addProjectAccess': kwargs})
+ url = 'types/%s/action' % volume_type_id
+ resp, body = self.post(url, post_body)
+ self.expected_success(202, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def remove_type_access(self, volume_type_id, **kwargs):
+ """Removes volume type access for the given project.
+
+ 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#remove-private-volume-type-access-from-project
+ """
+ post_body = json.dumps({'removeProjectAccess': kwargs})
+ url = 'types/%s/action' % volume_type_id
+ resp, body = self.post(url, post_body)
+ self.expected_success(202, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_type_access(self, volume_type_id):
+ """Print access information about the given volume type.
+
+ 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#list-private-volume-type-access-detail
+ """
+ url = 'types/%s/os-volume-type-access' % volume_type_id
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/volume/v3/volume_manage_client.py b/tempest/lib/services/volume/v3/volume_manage_client.py
new file mode 100644
index 0000000..349e11d
--- /dev/null
+++ b/tempest/lib/services/volume/v3/volume_manage_client.py
@@ -0,0 +1,35 @@
+# Copyright 2017 FiberHome Telecommunication Technologies CO.,LTD
+# 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 oslo_serialization import jsonutils as json
+
+from tempest.lib.common import rest_client
+
+
+class VolumeManageClient(rest_client.RestClient):
+ """Volume manage client."""
+
+ def manage_volume(self, **kwargs):
+ """Manage existing volume.
+
+ 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#manage-an-existing-volume
+ """
+ post_body = json.dumps({'volume': kwargs})
+ resp, body = self.post('os-volume-manage', post_body)
+ self.expected_success(202, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/volume/v3/volumes_client.py b/tempest/lib/services/volume/v3/volumes_client.py
index 5f4b278..a1185c4 100644
--- a/tempest/lib/services/volume/v3/volumes_client.py
+++ b/tempest/lib/services/volume/v3/volumes_client.py
@@ -14,16 +14,96 @@
# under the License.
from oslo_serialization import jsonutils as json
+import six
from six.moves.urllib import parse as urllib
from tempest.lib.common import rest_client
-from tempest.lib.services.volume.v2 import volumes_client
+from tempest.lib import exceptions as lib_exc
+from tempest.lib.services.volume import base_client
-class VolumesClient(volumes_client.VolumesClient):
+class VolumesClient(base_client.BaseClient):
"""Client class to send CRUD Volume V3 API requests"""
api_version = "v3"
+ def _prepare_params(self, params):
+ """Prepares params for use in get or _ext_get methods.
+
+ If params is a string it will be left as it is, but if it's not it will
+ be urlencoded.
+ """
+ if isinstance(params, six.string_types):
+ return params
+ return urllib.urlencode(params)
+
+ def list_volumes(self, detail=False, params=None):
+ """List all the volumes created.
+
+ Params can be a string (must be urlencoded) or a dictionary.
+ 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#list-accessible-volumes-with-details
+ https://developer.openstack.org/api-ref/block-storage/v3/index.html#list-accessible-volumes
+ """
+ url = 'volumes'
+ if detail:
+ url += '/detail'
+ if params:
+ url += '?%s' % self._prepare_params(params)
+
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_volume(self, volume_id):
+ """Returns the details of a single volume."""
+ url = "volumes/%s" % volume_id
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def create_volume(self, **kwargs):
+ """Creates a new Volume.
+
+ 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#create-a-volume
+ """
+ post_body = json.dumps({'volume': kwargs})
+ resp, body = self.post('volumes', post_body)
+ body = json.loads(body)
+ self.expected_success(202, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_volume(self, volume_id, **kwargs):
+ """Updates the Specified Volume.
+
+ 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#update-a-volume
+ """
+ put_body = json.dumps({'volume': kwargs})
+ resp, body = self.put('volumes/%s' % volume_id, put_body)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_volume(self, volume_id, **params):
+ """Deletes the Specified Volume.
+
+ 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#delete-a-volume
+ """
+ url = 'volumes/%s' % volume_id
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+ resp, body = self.delete(url)
+ self.expected_success(202, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
def show_volume_summary(self, **params):
"""Get volumes summary.
@@ -38,3 +118,250 @@
body = json.loads(body)
self.expected_success(200, resp.status)
return rest_client.ResponseBody(resp, body)
+
+ def upload_volume(self, volume_id, **kwargs):
+ """Uploads a volume in Glance."""
+ post_body = json.dumps({'os-volume_upload_image': kwargs})
+ url = 'volumes/%s/action' % (volume_id)
+ resp, body = self.post(url, post_body)
+ body = json.loads(body)
+ self.expected_success(202, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def attach_volume(self, volume_id, **kwargs):
+ """Attaches a volume to a given instance on a given mountpoint.
+
+ 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#attach-volume-to-a-server
+ """
+ post_body = json.dumps({'os-attach': kwargs})
+ url = 'volumes/%s/action' % (volume_id)
+ resp, body = self.post(url, post_body)
+ self.expected_success(202, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def set_bootable_volume(self, volume_id, **kwargs):
+ """Set a bootable flag for a volume - true or false.
+
+ 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#update-a-volume-s-bootable-status
+ """
+ post_body = json.dumps({'os-set_bootable': kwargs})
+ url = 'volumes/%s/action' % (volume_id)
+ resp, body = self.post(url, post_body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def detach_volume(self, volume_id):
+ """Detaches a volume from an instance."""
+ post_body = json.dumps({'os-detach': {}})
+ url = 'volumes/%s/action' % (volume_id)
+ resp, body = self.post(url, post_body)
+ self.expected_success(202, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def reserve_volume(self, volume_id):
+ """Reserves a volume."""
+ post_body = json.dumps({'os-reserve': {}})
+ url = 'volumes/%s/action' % (volume_id)
+ resp, body = self.post(url, post_body)
+ self.expected_success(202, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def unreserve_volume(self, volume_id):
+ """Restore a reserved volume ."""
+ post_body = json.dumps({'os-unreserve': {}})
+ url = 'volumes/%s/action' % (volume_id)
+ resp, body = self.post(url, post_body)
+ self.expected_success(202, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def is_resource_deleted(self, id):
+ """Check the specified resource is deleted or not.
+
+ :param id: A checked resource id
+ :raises lib_exc.DeleteErrorException: If the specified resource is on
+ the status the delete was failed.
+ """
+ try:
+ volume = self.show_volume(id)
+ except lib_exc.NotFound:
+ return True
+ if volume["volume"]["status"] == "error_deleting":
+ raise lib_exc.DeleteErrorException(resource_id=id)
+ return False
+
+ @property
+ def resource_type(self):
+ """Returns the primary type of resource this client works with."""
+ return 'volume'
+
+ def extend_volume(self, volume_id, **kwargs):
+ """Extend a volume.
+
+ 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#extend-a-volume-size
+ """
+ post_body = json.dumps({'os-extend': kwargs})
+ url = 'volumes/%s/action' % (volume_id)
+ resp, body = self.post(url, post_body)
+ self.expected_success(202, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def reset_volume_status(self, volume_id, **kwargs):
+ """Reset the Specified Volume's Status.
+
+ 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#reset-a-volume-s-statuses
+ """
+ post_body = json.dumps({'os-reset_status': kwargs})
+ resp, body = self.post('volumes/%s/action' % volume_id, post_body)
+ self.expected_success(202, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_volume_readonly(self, volume_id, **kwargs):
+ """Update the Specified Volume readonly."""
+ post_body = json.dumps({'os-update_readonly_flag': kwargs})
+ url = 'volumes/%s/action' % (volume_id)
+ resp, body = self.post(url, post_body)
+ self.expected_success(202, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def force_delete_volume(self, volume_id):
+ """Force Delete Volume."""
+ post_body = json.dumps({'os-force_delete': {}})
+ resp, body = self.post('volumes/%s/action' % volume_id, post_body)
+ self.expected_success(202, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def create_volume_metadata(self, volume_id, metadata):
+ """Create metadata for the volume.
+
+ 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#create-metadata-for-volume
+ """
+ put_body = json.dumps({'metadata': metadata})
+ url = "volumes/%s/metadata" % volume_id
+ resp, body = self.post(url, put_body)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_volume_metadata(self, volume_id):
+ """Get metadata of the volume."""
+ url = "volumes/%s/metadata" % volume_id
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_volume_metadata(self, volume_id, metadata):
+ """Update metadata for the volume.
+
+ 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#update-a-volume-s-metadata
+ """
+ put_body = json.dumps({'metadata': metadata})
+ url = "volumes/%s/metadata" % volume_id
+ resp, body = self.put(url, put_body)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_volume_metadata_item(self, volume_id, id):
+ """Show metadata item for the volume."""
+ url = "volumes/%s/metadata/%s" % (volume_id, id)
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_volume_metadata_item(self, volume_id, id, meta_item):
+ """Update metadata item for the volume."""
+ put_body = json.dumps({'meta': meta_item})
+ url = "volumes/%s/metadata/%s" % (volume_id, id)
+ resp, body = self.put(url, put_body)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_volume_metadata_item(self, volume_id, id):
+ """Delete metadata item for the volume."""
+ url = "volumes/%s/metadata/%s" % (volume_id, id)
+ resp, body = self.delete(url)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def retype_volume(self, volume_id, **kwargs):
+ """Updates volume with new volume type.
+
+ 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#retype-a-volume
+ """
+ post_body = json.dumps({'os-retype': kwargs})
+ resp, body = self.post('volumes/%s/action' % volume_id, post_body)
+ self.expected_success(202, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def force_detach_volume(self, volume_id, **kwargs):
+ """Force detach a volume.
+
+ 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#force-delete-a-volume
+ """
+ post_body = json.dumps({'os-force_detach': kwargs})
+ url = 'volumes/%s/action' % volume_id
+ resp, body = self.post(url, post_body)
+ self.expected_success(202, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_volume_image_metadata(self, volume_id, **kwargs):
+ """Update image metadata for the volume.
+
+ 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#set-image-metadata-for-a-volume
+ """
+ post_body = json.dumps({'os-set_image_metadata': {'metadata': kwargs}})
+ url = "volumes/%s/action" % (volume_id)
+ resp, body = self.post(url, post_body)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_volume_image_metadata(self, volume_id, key_name):
+ """Delete image metadata item for the volume."""
+ post_body = json.dumps({'os-unset_image_metadata': {'key': key_name}})
+ url = "volumes/%s/action" % (volume_id)
+ resp, body = self.post(url, post_body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_volume_image_metadata(self, volume_id):
+ """Show image metadata for the volume."""
+ post_body = json.dumps({'os-show_image_metadata': {}})
+ url = "volumes/%s/action" % volume_id
+ resp, body = self.post(url, post_body)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def unmanage_volume(self, volume_id):
+ """Unmanage volume.
+
+ 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#unmanage-a-volume
+ """
+ post_body = json.dumps({'os-unmanage': {}})
+ resp, body = self.post('volumes/%s/action' % volume_id, post_body)
+ self.expected_success(202, resp.status)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/scenario/manager.py b/tempest/scenario/manager.py
index c78646f..9965fe5 100644
--- a/tempest/scenario/manager.py
+++ b/tempest/scenario/manager.py
@@ -83,6 +83,7 @@
if CONF.service_available.cinder:
cls.volumes_client = cls.os_primary.volumes_client_latest
cls.snapshots_client = cls.os_primary.snapshots_client_latest
+ cls.backups_client = cls.os_primary.backups_client_latest
# ## Test functions library
#
@@ -244,6 +245,37 @@
volume = self.volumes_client.show_volume(volume['id'])['volume']
return volume
+ def create_backup(self, volume_id, name=None, description=None,
+ force=False, snapshot_id=None, incremental=False,
+ container=None):
+
+ name = name or data_utils.rand_name(
+ self.__class__.__name__ + "-backup")
+ kwargs = {'name': name,
+ 'description': description,
+ 'force': force,
+ 'snapshot_id': snapshot_id,
+ 'incremental': incremental,
+ 'container': container}
+ backup = self.backups_client.create_backup(volume_id=volume_id,
+ **kwargs)['backup']
+ self.addCleanup(self.backups_client.delete_backup, backup['id'])
+ waiters.wait_for_volume_resource_status(self.backups_client,
+ backup['id'], 'available')
+ return backup
+
+ def restore_backup(self, backup_id):
+ restore = self.backups_client.restore_backup(backup_id)['restore']
+ self.addCleanup(self.volumes_client.delete_volume,
+ restore['volume_id'])
+ waiters.wait_for_volume_resource_status(self.backups_client,
+ backup_id, 'available')
+ waiters.wait_for_volume_resource_status(self.volumes_client,
+ restore['volume_id'],
+ 'available')
+ self.assertEqual(backup_id, restore['backup_id'])
+ return restore
+
def create_volume_snapshot(self, volume_id, name=None, description=None,
metadata=None, force=False):
name = name or data_utils.rand_name(
@@ -266,10 +298,8 @@
def create_volume_type(self, client=None, name=None, backend_name=None):
if not client:
client = self.os_admin.volume_types_v2_client
- if not name:
- class_name = self.__class__.__name__
- name = data_utils.rand_name(class_name + '-volume-type')
- randomized_name = data_utils.rand_name('scenario-type-' + name)
+ randomized_name = name or data_utils.rand_name(
+ 'volume-type-' + self.__class__.__name__)
LOG.debug("Creating a volume type: %s on backend %s",
randomized_name, backend_name)
@@ -662,8 +692,8 @@
addresses = (server['addresses'][network['name']]
if network else [])
for address in addresses:
- if (address['version'] == CONF.validation.ip_version_for_ssh
- and address['OS-EXT-IPS:type'] == 'fixed'):
+ if (address['version'] == CONF.validation.ip_version_for_ssh and # noqa
+ address['OS-EXT-IPS:type'] == 'fixed'):
return address['addr']
raise exceptions.ServerUnreachable(server_id=server['id'])
else:
@@ -792,8 +822,8 @@
port_map = [(p["id"], fxip["ip_address"])
for p in ports
for fxip in p["fixed_ips"]
- if netutils.is_valid_ipv4(fxip["ip_address"])
- and p['status'] in p_status]
+ if (netutils.is_valid_ipv4(fxip["ip_address"]) and
+ p['status'] in p_status)]
inactive = [p for p in ports if p['status'] != 'ACTIVE']
if inactive:
LOG.warning("Instance has ports that are not ACTIVE: %s", inactive)
diff --git a/tempest/scenario/test_aggregates_basic_ops.py b/tempest/scenario/test_aggregates_basic_ops.py
index f762995..b515639 100644
--- a/tempest/scenario/test_aggregates_basic_ops.py
+++ b/tempest/scenario/test_aggregates_basic_ops.py
@@ -37,7 +37,7 @@
super(TestAggregatesBasicOps, cls).setup_clients()
# Use admin client by default
cls.aggregates_client = cls.os_admin.aggregates_client
- cls.hyper_client = cls.os_admin.hypervisor_client
+ cls.services_client = cls.os_admin.services_client
def _create_aggregate(self, **kwargs):
aggregate = (self.aggregates_client.create_aggregate(**kwargs)
@@ -51,9 +51,10 @@
return aggregate
def _get_host_name(self):
- hyper_list = self.hyper_client.list_hypervisors()['hypervisors']
- self.assertNotEmpty(hyper_list)
- return hyper_list[0]['hypervisor_hostname']
+ svc_list = self.services_client.list_services(
+ binary='nova-compute')['services']
+ self.assertNotEmpty(svc_list)
+ return svc_list[0]['host']
def _add_host(self, aggregate_id, host):
aggregate = (self.aggregates_client.add_host(aggregate_id, host=host)
diff --git a/tempest/scenario/test_encrypted_cinder_volumes.py b/tempest/scenario/test_encrypted_cinder_volumes.py
index b5220e9..8c210d5 100644
--- a/tempest/scenario/test_encrypted_cinder_volumes.py
+++ b/tempest/scenario/test_encrypted_cinder_volumes.py
@@ -57,8 +57,7 @@
@utils.services('compute', 'volume', 'image')
def test_encrypted_cinder_volumes_luks(self):
server = self.launch_instance()
- volume = self.create_encrypted_volume('nova.volume.encryptors.'
- 'luks.LuksEncryptor',
+ volume = self.create_encrypted_volume('luks',
volume_type='luks')
self.attach_detach_volume(server, volume)
@@ -67,7 +66,6 @@
@utils.services('compute', 'volume', 'image')
def test_encrypted_cinder_volumes_cryptsetup(self):
server = self.launch_instance()
- volume = self.create_encrypted_volume('nova.volume.encryptors.'
- 'cryptsetup.CryptsetupEncryptor',
+ volume = self.create_encrypted_volume('plain',
volume_type='cryptsetup')
self.attach_detach_volume(server, volume)
diff --git a/tempest/scenario/test_network_advanced_server_ops.py b/tempest/scenario/test_network_advanced_server_ops.py
index e4ab11c..87ce951 100644
--- a/tempest/scenario/test_network_advanced_server_ops.py
+++ b/tempest/scenario/test_network_advanced_server_ops.py
@@ -43,8 +43,8 @@
@classmethod
def skip_checks(cls):
super(TestNetworkAdvancedServerOps, cls).skip_checks()
- if not (CONF.network.project_networks_reachable
- or CONF.network.public_network_id):
+ if not (CONF.network.project_networks_reachable or
+ CONF.network.public_network_id):
msg = ('Either project_networks_reachable must be "true", or '
'public_network_id must be defined.')
raise cls.skipException(msg)
diff --git a/tempest/scenario/test_network_basic_ops.py b/tempest/scenario/test_network_basic_ops.py
index fd9c985..bcd4ddb 100644
--- a/tempest/scenario/test_network_basic_ops.py
+++ b/tempest/scenario/test_network_basic_ops.py
@@ -81,8 +81,8 @@
@classmethod
def skip_checks(cls):
super(TestNetworkBasicOps, cls).skip_checks()
- if not (CONF.network.project_networks_reachable
- or CONF.network.public_network_id):
+ if not (CONF.network.project_networks_reachable or
+ CONF.network.public_network_id):
msg = ('Either project_networks_reachable must be "true", or '
'public_network_id must be defined.')
raise cls.skipException(msg)
diff --git a/tempest/scenario/test_network_v6.py b/tempest/scenario/test_network_v6.py
index 9f4e62b..e4e39c3 100644
--- a/tempest/scenario/test_network_v6.py
+++ b/tempest/scenario/test_network_v6.py
@@ -38,11 +38,11 @@
@classmethod
def skip_checks(cls):
super(TestGettingAddress, cls).skip_checks()
- if not (CONF.network_feature_enabled.ipv6
- and CONF.network_feature_enabled.ipv6_subnet_attributes):
+ if not (CONF.network_feature_enabled.ipv6 and
+ CONF.network_feature_enabled.ipv6_subnet_attributes):
raise cls.skipException('IPv6 or its attributes not supported')
- if not (CONF.network.project_networks_reachable
- or CONF.network.public_network_id):
+ if not (CONF.network.project_networks_reachable or
+ CONF.network.public_network_id):
msg = ('Either project_networks_reachable must be "true", or '
'public_network_id must be defined.')
raise cls.skipException(msg)
diff --git a/tempest/scenario/test_server_advanced_ops.py b/tempest/scenario/test_server_advanced_ops.py
index 89b9fdd..8aa729b 100644
--- a/tempest/scenario/test_server_advanced_ops.py
+++ b/tempest/scenario/test_server_advanced_ops.py
@@ -32,7 +32,6 @@
"""The test suite for server advanced operations
This test case stresses some advanced server instance operations:
- * Resizing a volume-backed instance
* Sequence suspend resume
"""
diff --git a/tempest/scenario/test_volume_backup_restore.py b/tempest/scenario/test_volume_backup_restore.py
new file mode 100644
index 0000000..c23b564
--- /dev/null
+++ b/tempest/scenario/test_volume_backup_restore.py
@@ -0,0 +1,91 @@
+# Copyright 2018 Red Hat, Inc.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from tempest.common import utils
+from tempest import config
+from tempest.lib import decorators
+from tempest.scenario import manager
+
+CONF = config.CONF
+
+
+class TestVolumeBackupRestore(manager.ScenarioTest):
+ """Test cinder backup and restore
+
+ This testcase verifies content preservation after backup and restore
+ operations by booting a server from a restored backup and check the
+ connectivity to it.
+
+ The following is the scenario outline:
+ 1. Create volume from image.
+ 2. Create a backup for the volume.
+ 3. Restore the backup.
+ 4. Boot a server from the restored backup.
+ 5. Create a floating ip.
+ 6. Check server connectivity.
+ """
+
+ @classmethod
+ def skip_checks(cls):
+ super(TestVolumeBackupRestore, cls).skip_checks()
+ if not CONF.volume_feature_enabled.backup:
+ raise cls.skipException('Backup is not enable.')
+
+ @decorators.idempotent_id('2ce5e55c-4085-43c1-98c6-582525334ad7')
+ @decorators.attr(type='slow')
+ @utils.services('compute', 'volume', 'image')
+ def test_volume_backup_restore(self):
+ # Create volume from image
+ img_uuid = CONF.compute.image_ref
+ volume = self.create_volume(imageRef=img_uuid)
+ volume_details = self.volumes_client.show_volume(
+ volume['id'])['volume']
+ self.assertEqual('true', volume_details['bootable'])
+
+ # Create a backup
+ backup = self.create_backup(volume_id=volume['id'])
+
+ # Restore the backup
+ restored_volume_id = self.restore_backup(backup['id'])['volume_id']
+
+ # Verify the restored backup volume is bootable
+ restored_volume_info = self.volumes_client.show_volume(
+ restored_volume_id)['volume']
+ self.assertEqual('true', restored_volume_info['bootable'])
+
+ # Create keypair and security group
+ keypair = self.create_keypair()
+ security_group = self._create_security_group()
+
+ # Boot a server from the restored backup
+ bd_map_v2 = [{
+ 'uuid': restored_volume_id,
+ 'source_type': 'volume',
+ 'destination_type': 'volume',
+ 'boot_index': 0}]
+ server = self.create_server(image_id='',
+ block_device_mapping_v2=bd_map_v2,
+ key_name=keypair['name'],
+ security_groups=[
+ {'name': security_group['name']}])
+
+ # Create a floating ip
+ floating_ip = self.create_floating_ip(server)
+
+ # Check server connectivity
+ self.check_vm_connectivity(floating_ip['ip'],
+ username=CONF.validation.image_ssh_user,
+ private_key=keypair['private_key'],
+ should_connect=True)
diff --git a/tempest/scenario/test_volume_boot_pattern.py b/tempest/scenario/test_volume_boot_pattern.py
index 7ceae89..2d024e9 100644
--- a/tempest/scenario/test_volume_boot_pattern.py
+++ b/tempest/scenario/test_volume_boot_pattern.py
@@ -193,38 +193,54 @@
@testtools.skipUnless(CONF.volume_feature_enabled.snapshot,
'Cinder volume snapshots are disabled')
@utils.services('compute', 'volume', 'image')
- def test_create_ebs_image_and_check_boot(self):
- # create an instance from volume
+ def test_image_defined_boot_from_volume(self):
+ # create an instance from image-backed volume
volume_origin = self._create_volume_from_image()
instance = self._boot_instance_from_resource(
source_id=volume_origin['id'],
source_type='volume',
delete_on_termination=True)
- # create EBS image
+ # Create a snapshot image from the volume-backed server.
+ # The compute service will have the block service create a snapshot of
+ # the root volume and store its metadata in the image.
image = self.create_server_snapshot(instance)
- # delete instance
+ # Delete the first server which will also delete the first image-backed
+ # volume.
self._delete_server(instance)
- # boot instance from EBS image
+ # Create a server from the image snapshot which has an
+ # "image-defined block device mapping (BDM)" in it, i.e. the metadata
+ # about the volume snapshot. The compute service will use this to
+ # create a volume from the volume snapshot and use that as the root
+ # disk for the server.
instance = self.create_server(image_id=image['id'])
- # Verify the server was created from the image
- created_volume = instance['os-extended-volumes:volumes_attached']
- self.assertNotEmpty(created_volume, "No volume attachment found.")
- created_volume_info = self.volumes_client.show_volume(
- created_volume[0]['id'])['volume']
+ # Verify the server was created from the image-defined BDM.
+ volume_attachments = instance['os-extended-volumes:volumes_attached']
+ self.assertEqual(1, len(volume_attachments),
+ "No volume attachment found.")
+ created_volume = self.volumes_client.show_volume(
+ volume_attachments[0]['id'])['volume']
+ # Assert that the volume service also shows the server attachment.
+ self.assertEqual(1, len(created_volume['attachments']),
+ "No server attachment found for volume: %s" %
+ created_volume)
self.assertEqual(instance['id'],
- created_volume_info['attachments'][0]['server_id'])
- self.assertEqual(created_volume[0]['id'],
- created_volume_info['attachments'][0]['volume_id'])
+ created_volume['attachments'][0]['server_id'])
+ self.assertEqual(volume_attachments[0]['id'],
+ created_volume['attachments'][0]['volume_id'])
self.assertEqual(
volume_origin['volume_image_metadata']['image_id'],
- created_volume_info['volume_image_metadata']['image_id'])
+ created_volume['volume_image_metadata']['image_id'])
- # delete instance
+ # Delete the second server which should also delete the second volume
+ # created from the volume snapshot.
self._delete_server(instance)
+ # Assert that the underlying volume is gone.
+ self.volumes_client.wait_for_resource_deletion(created_volume['id'])
+
@decorators.idempotent_id('cb78919a-e553-4bab-b73b-10cf4d2eb125')
@testtools.skipUnless(CONF.compute_feature_enabled.attach_encrypted_volume,
'Encrypted volume attach is not supported')
diff --git a/tempest/test.py b/tempest/test.py
index 27e0165..f2babbb 100644
--- a/tempest/test.py
+++ b/tempest/test.py
@@ -68,9 +68,9 @@
def validate_tearDownClass():
if at_exit_set:
LOG.error(
- "tearDownClass does not call the super's "
- "tearDownClass in these classes: \n"
- + str(at_exit_set))
+ "tearDownClass does not call the super's tearDownClass in "
+ "these classes:\n"
+ " %s", at_exit_set)
atexit.register(validate_tearDownClass)
@@ -582,8 +582,8 @@
super(BaseTestCase, self).setUp()
if not self.__setupclass_called:
raise RuntimeError("setUpClass does not calls the super's"
- "setUpClass in the "
- + self.__class__.__name__)
+ "setUpClass in the " +
+ self.__class__.__name__)
at_exit_set.add(self.__class__)
test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
try:
@@ -602,7 +602,7 @@
stderr = self.useFixture(fixtures.StringStream('stderr')).stream
self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
if (os.environ.get('OS_LOG_CAPTURE') != 'False' and
- os.environ.get('OS_LOG_CAPTURE') != '0'):
+ os.environ.get('OS_LOG_CAPTURE') != '0'):
self.useFixture(fixtures.LoggerFixture(nuke_handlers=False,
format=self.log_format,
level=None))
diff --git a/tempest/tests/api/compute/test_base.py b/tempest/tests/api/compute/test_base.py
index 5024100..47f4ad6 100644
--- a/tempest/tests/api/compute/test_base.py
+++ b/tempest/tests/api/compute/test_base.py
@@ -173,3 +173,20 @@
# make our assertions
wait_for_image_status.assert_called_once_with(
compute_images_client, image_id, 'SAVING')
+
+ def _test_version_compatible(self, max_version, expected=True):
+ actual = (compute_base.BaseV2ComputeTest.
+ is_requested_microversion_compatible(max_version))
+ self.assertEqual(expected, actual)
+
+ def test_check_lower_version(self):
+ compute_base.BaseV2ComputeTest.request_microversion = '2.8'
+ self._test_version_compatible('2.40')
+
+ def test_check_euqal_version(self):
+ compute_base.BaseV2ComputeTest.request_microversion = '2.40'
+ self._test_version_compatible('2.40')
+
+ def test_check_higher_version(self):
+ compute_base.BaseV2ComputeTest.request_microversion = '2.41'
+ self._test_version_compatible('2.40', expected=False)
diff --git a/tempest/tests/base.py b/tempest/tests/base.py
index ca81d4d..0b53b45 100644
--- a/tempest/tests/base.py
+++ b/tempest/tests/base.py
@@ -18,7 +18,7 @@
class TestCase(base.BaseTestCase):
- def patch(self, target, **kwargs):
+ def patch(self, target, *args, **kwargs):
"""Returns a started `mock.patch` object for the supplied target.
The caller may then call the returned patcher to create a mock object.
@@ -27,23 +27,35 @@
patcher object, as this method automatically adds a cleanup
to the test class to stop the patcher.
- :param target: String module.class or module.object expression to patch
- :param **kwargs: Passed as-is to `mock.patch`. See mock documentation
- for details.
+ :param target: string module.class or module.object expression to patch
+ :param *args: passed as-is to `mock.patch`.
+ :param **kwargs: passed as-is to `mock.patch`.
+
+ See mock documentation for more details:
+ https://docs.python.org/3.5/library/unittest.mock.html#unittest.mock.patch
"""
- p = mock.patch(target, **kwargs)
+
+ p = mock.patch(target, *args, **kwargs)
m = p.start()
self.addCleanup(p.stop)
return m
- def patchobject(self, target, attribute, new=mock.DEFAULT):
+ def patchobject(self, target, attribute, *args, **kwargs):
"""Convenient wrapper around `mock.patch.object`
Returns a started mock that will be automatically stopped after the
test ran.
+
+ :param target: object to have the attribute patched
+ :param attribute: name of the attribute to be patched
+ :param *args: passed as-is to `mock.patch.object`.
+ :param **kwargs: passed as-is to `mock.patch.object`.
+
+ See mock documentation for more details:
+ https://docs.python.org/3.5/library/unittest.mock.html#unittest.mock.patch.object
"""
- p = mock.patch.object(target, attribute, new)
+ p = mock.patch.object(target, attribute, *args, **kwargs)
m = p.start()
self.addCleanup(p.stop)
return m
diff --git a/tempest/tests/common/test_waiters.py b/tempest/tests/common/test_waiters.py
index bc197b5..eb1e2b6 100644
--- a/tempest/tests/common/test_waiters.py
+++ b/tempest/tests/common/test_waiters.py
@@ -72,3 +72,64 @@
mock_show.assert_has_calls([mock.call(volume_id),
mock.call(volume_id)])
mock_sleep.assert_called_once_with(1)
+
+
+class TestInterfaceWaiters(base.TestCase):
+ def setUp(self):
+ super(TestInterfaceWaiters, self).setUp()
+ self.client = mock.MagicMock()
+ self.client.build_timeout = 1
+ self.client.build_interval = 1
+
+ def _port_down(self):
+ return {'interfaceAttachment': {'port_state': 'DOWN'}}
+
+ def _port_active(self):
+ return {'interfaceAttachment': {'port_state': 'ACTIVE'}}
+
+ def test_wait_for_interface_status(self):
+ self.client.show_interface.side_effect = [self._port_down(),
+ self._port_active()]
+ with mock.patch.object(time, 'sleep') as sleep_mock:
+ start_time = int(time.time())
+ waiters.wait_for_interface_status(self.client, 'server_id',
+ 'port_id', 'ACTIVE')
+ end_time = int(time.time())
+ self.assertLess(end_time, (start_time + self.client.build_timeout))
+ sleep_mock.assert_called_once_with(self.client.build_interval)
+
+ def test_wait_for_interface_status_timeout(self):
+ time_mock = self.patch('time.time')
+ time_mock.side_effect = utils.generate_timeout_series(1)
+
+ self.client.show_interface.return_value = self._port_down()
+ self.assertRaises(lib_exc.TimeoutException,
+ waiters.wait_for_interface_status,
+ self.client, 'server_id', 'port_id', 'ACTIVE')
+
+ def _one_interface(self):
+ return {'interfaceAttachments': [{'port_id': 'port_one'}]}
+
+ def _two_interfaces(self):
+ return {'interfaceAttachments': [{'port_id': 'port_one'},
+ {'port_id': 'port_two'}]}
+
+ def test_wait_for_interface_detach(self):
+ self.client.list_interfaces.side_effect = [self._two_interfaces(),
+ self._one_interface()]
+ with mock.patch.object(time, 'sleep') as sleep_mock:
+ start_time = int(time.time())
+ waiters.wait_for_interface_detach(self.client, 'server_id',
+ 'port_two')
+ end_time = int(time.time())
+ self.assertLess(end_time, (start_time + self.client.build_timeout))
+ sleep_mock.assert_called_once_with(self.client.build_interval)
+
+ def test_wait_for_interface_detach_timeout(self):
+ time_mock = self.patch('time.time')
+ time_mock.side_effect = utils.generate_timeout_series(1)
+
+ self.client.list_interfaces.return_value = self._one_interface()
+ self.assertRaises(lib_exc.TimeoutException,
+ waiters.wait_for_interface_detach,
+ self.client, 'server_id', 'port_one')
diff --git a/tempest/tests/lib/services/compute/test_flavors_client.py b/tempest/tests/lib/services/compute/test_flavors_client.py
index cbd17c6..5325036 100644
--- a/tempest/tests/lib/services/compute/test_flavors_client.py
+++ b/tempest/tests/lib/services/compute/test_flavors_client.py
@@ -17,6 +17,7 @@
import fixtures
from oslo_serialization import jsonutils as json
+from tempest.api.compute import api_microversion_fixture
from tempest.lib.services.compute import flavors_client
from tempest.tests.lib import fake_auth_provider
from tempest.tests.lib import fake_http
@@ -39,6 +40,21 @@
"vcpus": 1
}
+ FAKE_FLAVOR_UPDATE = {
+ "disk": 1,
+ "id": "1",
+ "links": [{
+ "href": "http://openstack.example.com/v2/openstack/flavors/1",
+ "rel": "self"}, {
+ "href": "http://openstack.example.com/openstack/flavors/1",
+ "rel": "bookmark"}],
+ "name": "m1.tiny",
+ "ram": 512,
+ "swap": 1,
+ "vcpus": 1,
+ "description": 'new'
+ }
+
EXTRA_SPECS = {"extra_specs": {
"key1": "value1",
"key2": "value2"}
@@ -106,6 +122,25 @@
def test_create_flavor__byte_body(self):
self._test_create_flavor(bytes_body=True)
+ def _test_update_flavor(self, bytes_body=False):
+ self.useFixture(api_microversion_fixture.APIMicroversionFixture(
+ '2.55'))
+ expected = {"flavor": TestFlavorsClient.FAKE_FLAVOR_UPDATE}
+ request = {"flavor": {"description": "updated description"}}
+ self.check_service_client_function(
+ self.client.update_flavor,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ expected,
+ bytes_body,
+ flavor_id='8c7aae5a-d315-4216-875b-ed9b6a5bcfc6',
+ **request)
+
+ def test_update_flavor_str_body(self):
+ self._test_update_flavor(bytes_body=False)
+
+ def test_update_flavor__byte_body(self):
+ self._test_update_flavor(bytes_body=True)
+
def test_delete_flavor(self):
self.check_service_client_function(
self.client.delete_flavor,
diff --git a/tempest/tests/lib/services/identity/v3/test_application_credentials_client.py b/tempest/tests/lib/services/identity/v3/test_application_credentials_client.py
new file mode 100644
index 0000000..9bf9b68
--- /dev/null
+++ b/tempest/tests/lib/services/identity/v3/test_application_credentials_client.py
@@ -0,0 +1,156 @@
+# Copyright 2018 SUSE Linux GmbH
+#
+# 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.lib.services.identity.v3 import application_credentials_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestApplicationCredentialsClient(base.BaseServiceTest):
+ FAKE_CREATE_APP_CRED = {
+ "application_credential": {
+ "description": "fake application credential",
+ "roles": [
+ {
+ "id": "c60fdd45",
+ "domain_id": None,
+ "name": "Member"
+ }
+ ],
+ "expires_at": "2019-02-27T18:30:59.999999Z",
+ "secret": "_BVq0xU5L",
+ "unrestricted": None,
+ "project_id": "ddef321",
+ "id": "5499a186",
+ "name": "one"
+ }
+ }
+
+ FAKE_LIST_APP_CREDS = {
+ "application_credentials": [
+ {
+ "description": "fake application credential",
+ "roles": [
+ {
+ "domain_id": None,
+ "name": "Member",
+ "id": "c60fdd45",
+ }
+ ],
+ "expires_at": "2018-02-27T18:30:59.999999Z",
+ "unrestricted": None,
+ "project_id": "ddef321",
+ "id": "5499a186",
+ "name": "one"
+ },
+ {
+ "description": None,
+ "roles": [
+ {
+ "id": "0f1837c8",
+ "domain_id": None,
+ "name": "anotherrole"
+ },
+ {
+ "id": "c60fdd45",
+ "domain_id": None,
+ "name": "Member"
+ }
+ ],
+ "expires_at": None,
+ "unrestricted": None,
+ "project_id": "c5403d938",
+ "id": "d441c904f",
+ "name": "two"
+ }
+ ]
+ }
+
+ FAKE_APP_CRED_INFO = {
+ "application_credential": {
+ "description": None,
+ "roles": [
+ {
+ "domain_id": None,
+ "name": "Member",
+ "id": "c60fdd45",
+ }
+ ],
+ "expires_at": None,
+ "unrestricted": None,
+ "project_id": "ddef321",
+ "id": "5499a186",
+ "name": "one"
+ }
+ }
+
+ def setUp(self):
+ super(TestApplicationCredentialsClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = \
+ application_credentials_client.ApplicationCredentialsClient(
+ fake_auth, 'identity', 'regionOne')
+
+ def _test_create_app_cred(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.create_application_credential,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ self.FAKE_CREATE_APP_CRED,
+ bytes_body,
+ status=201,
+ user_id="123456")
+
+ def _test_show_app_cred(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_application_credential,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_APP_CRED_INFO,
+ bytes_body,
+ user_id="123456",
+ application_credential_id="5499a186")
+
+ def _test_list_app_creds(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_application_credentials,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_LIST_APP_CREDS,
+ bytes_body,
+ user_id="123456")
+
+ def test_create_application_credential_with_str_body(self):
+ self._test_create_app_cred()
+
+ def test_create_application_credential_with_bytes_body(self):
+ self._test_create_app_cred(bytes_body=True)
+
+ def test_show_application_credential_with_str_body(self):
+ self._test_show_app_cred()
+
+ def test_show_application_credential_with_bytes_body(self):
+ self._test_show_app_cred(bytes_body=True)
+
+ def test_list_application_credential_with_str_body(self):
+ self._test_list_app_creds()
+
+ def test_list_application_credential_with_bytes_body(self):
+ self._test_list_app_creds(bytes_body=True)
+
+ def test_delete_trust(self):
+ self.check_service_client_function(
+ self.client.delete_application_credential,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ user_id="123456",
+ application_credential_id="5499a186",
+ status=204)
diff --git a/tempest/tests/lib/services/identity/v3/test_project_tags_client.py b/tempest/tests/lib/services/identity/v3/test_project_tags_client.py
new file mode 100644
index 0000000..2d65a29
--- /dev/null
+++ b/tempest/tests/lib/services/identity/v3/test_project_tags_client.py
@@ -0,0 +1,104 @@
+# Copyright 2018 AT&T Corporation.
+#
+# 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.lib.services.identity.v3 import project_tags_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestProjectTagsClient(base.BaseServiceTest):
+
+ FAKE_PROJECT_ID = "0c4e939acacf4376bdcd1129f1a054ad"
+
+ FAKE_PROJECT_TAG = "foo"
+
+ FAKE_PROJECT_TAGS = ["foo", "bar"]
+
+ def setUp(self):
+ super(TestProjectTagsClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = project_tags_client.ProjectTagsClient(fake_auth,
+ 'identity',
+ 'regionOne')
+
+ def _test_update_project_tag(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.update_project_tag,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ {},
+ bytes_body,
+ project_id=self.FAKE_PROJECT_ID,
+ tag=self.FAKE_PROJECT_TAG,
+ status=201)
+
+ def _test_list_project_tags(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_project_tags,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ {"tags": self.FAKE_PROJECT_TAGS},
+ bytes_body,
+ project_id=self.FAKE_PROJECT_ID)
+
+ def _test_update_all_project_tags(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.update_all_project_tags,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ {"tags": self.FAKE_PROJECT_TAGS},
+ bytes_body,
+ project_id=self.FAKE_PROJECT_ID,
+ tags=self.FAKE_PROJECT_TAGS)
+
+ def test_update_project_tag_with_str_body(self):
+ self._test_update_project_tag()
+
+ def test_update_project_tag_with_bytes_body(self):
+ self._test_update_project_tag(bytes_body=True)
+
+ def test_list_project_tags_with_str_body(self):
+ self._test_list_project_tags()
+
+ def test_list_project_tags_with_bytes_body(self):
+ self._test_list_project_tags(bytes_body=True)
+
+ def test_update_all_project_tags_with_str_body(self):
+ self._test_update_all_project_tags()
+
+ def test_update_all_project_tags_with_bytes_body(self):
+ self._test_update_all_project_tags(bytes_body=True)
+
+ def test_check_project_project_tag_existence(self):
+ self.check_service_client_function(
+ self.client.check_project_tag_existence,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ {},
+ project_id=self.FAKE_PROJECT_ID,
+ tag=self.FAKE_PROJECT_TAG,
+ status=204)
+
+ def test_delete_project_tag(self):
+ self.check_service_client_function(
+ self.client.delete_project_tag,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ project_id=self.FAKE_PROJECT_ID,
+ tag=self.FAKE_PROJECT_TAG,
+ status=204)
+
+ def test_delete_all_project_tags(self):
+ self.check_service_client_function(
+ self.client.delete_all_project_tags,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ project_id=self.FAKE_PROJECT_ID,
+ status=204)
diff --git a/tempest/tests/lib/services/volume/v2/test_snapshot_manage_client.py b/tempest/tests/lib/services/volume/v2/test_snapshot_manage_client.py
index 3fe8970..e03a8eb 100644
--- a/tempest/tests/lib/services/volume/v2/test_snapshot_manage_client.py
+++ b/tempest/tests/lib/services/volume/v2/test_snapshot_manage_client.py
@@ -18,6 +18,8 @@
from oslo_serialization import jsonutils as json
from tempest.lib.services.volume.v2 import snapshot_manage_client
+from tempest.lib.services.volume.v3 import snapshot_manage_client \
+ as snapshot_manage_clientv3
from tempest.tests.lib import fake_auth_provider
from tempest.tests.lib.services import base
@@ -63,7 +65,7 @@
# NOTE: Use sort_keys for json.dumps so that the expected and actual
# payloads are guaranteed to be identical for mock_args assert check.
- with mock.patch.object(snapshot_manage_client.json,
+ with mock.patch.object(snapshot_manage_clientv3.json,
'dumps') as mock_dumps:
mock_dumps.side_effect = lambda d: json_dumps(d, sort_keys=True)
diff --git a/tempest/tests/lib/services/volume/v2/test_transfers_client.py b/tempest/tests/lib/services/volume/v2/test_transfers_client.py
index 84f4992..8e7c6f4 100644
--- a/tempest/tests/lib/services/volume/v2/test_transfers_client.py
+++ b/tempest/tests/lib/services/volume/v2/test_transfers_client.py
@@ -19,6 +19,8 @@
from oslo_serialization import jsonutils as json
from tempest.lib.services.volume.v2 import transfers_client
+from tempest.lib.services.volume.v3 import transfers_client \
+ as transfers_clientv3
from tempest.tests.lib import fake_auth_provider
from tempest.tests.lib.services import base
@@ -63,7 +65,7 @@
# NOTE: Use sort_keys for json.dumps so that the expected and actual
# payloads are guaranteed to be identical for mock_args assert check.
- with mock.patch.object(transfers_client.json, 'dumps') as mock_dumps:
+ with mock.patch.object(transfers_clientv3.json, 'dumps') as mock_dumps:
mock_dumps.side_effect = lambda d: json_dumps(d, sort_keys=True)
self.check_service_client_function(
@@ -84,7 +86,7 @@
# NOTE: Use sort_keys for json.dumps so that the expected and actual
# payloads are guaranteed to be identical for mock_args assert check.
- with mock.patch.object(transfers_client.json, 'dumps') as mock_dumps:
+ with mock.patch.object(transfers_clientv3.json, 'dumps') as mock_dumps:
mock_dumps.side_effect = lambda d: json_dumps(d, sort_keys=True)
self.check_service_client_function(
diff --git a/tempest/tests/lib/services/volume/v2/test_volume_manage_client.py b/tempest/tests/lib/services/volume/v2/test_volume_manage_client.py
index ea4a9f9..0fb66bb 100644
--- a/tempest/tests/lib/services/volume/v2/test_volume_manage_client.py
+++ b/tempest/tests/lib/services/volume/v2/test_volume_manage_client.py
@@ -18,6 +18,8 @@
from oslo_serialization import jsonutils as json
from tempest.lib.services.volume.v2 import volume_manage_client
+from tempest.lib.services.volume.v3 import volume_manage_client \
+ as volume_manage_clientv3
from tempest.tests.lib import fake_auth_provider
from tempest.tests.lib.services import base
@@ -91,7 +93,7 @@
# NOTE: Use sort_keys for json.dumps so that the expected and actual
# payloads are guaranteed to be identical for mock_args assert check.
- with mock.patch.object(volume_manage_client.json,
+ with mock.patch.object(volume_manage_clientv3.json,
'dumps') as mock_dumps:
mock_dumps.side_effect = lambda d: json_dumps(d, sort_keys=True)
diff --git a/tools/generate-tempest-plugins-list.py b/tools/generate-tempest-plugins-list.py
index dd05438..bbb9019 100644
--- a/tools/generate-tempest-plugins-list.py
+++ b/tools/generate-tempest-plugins-list.py
@@ -75,11 +75,13 @@
# json library won't choke.
projects = sorted(filter(is_in_openstack_namespace, json.loads(r.read()[4:])))
-# Retrieve projects having no deb, ui or spec namespace as those namespaces
-# do not contains tempest plugins.
-projects_list = [i for i in projects if not (i.startswith('openstack/deb-') or
- i.endswith('-ui') or
- i.endswith('-specs'))]
+# Retrieve projects having no deb, puppet, ui or spec namespace as those
+# namespaces do not contains tempest plugins.
+projects_list = [i for i in projects if not (
+ i.startswith('openstack/deb-') or
+ i.startswith('openstack/puppet-') or
+ i.endswith('-ui') or
+ i.endswith('-specs'))]
found_plugins = list(filter(has_tempest_plugin, projects_list))
diff --git a/tox.ini b/tox.ini
index 9103175..da0233a 100644
--- a/tox.ini
+++ b/tox.ini
@@ -83,6 +83,16 @@
tempest run --regex '(?!.*\[.*\bslow\b.*\])(^tempest\.api)' {posargs}
tempest run --combine --serial --regex '(?!.*\[.*\bslow\b.*\])(^tempest\.scenario)' {posargs}
+[testenv:full-parallel]
+envdir = .tox/tempest
+sitepackages = {[tempestenv]sitepackages}
+setenv = {[tempestenv]setenv}
+deps = {[tempestenv]deps}
+# The regex below is used to select all tempest scenario and including the non slow api tests
+commands =
+ find . -type f -name "*.pyc" -delete
+ tempest run --regex '(^tempest\.scenario.*)|(?!.*\[.*\bslow\b.*\])(^tempest\.api)' {posargs}
+
[testenv:full-serial]
envdir = .tox/tempest
sitepackages = {[tempestenv]sitepackages}